mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
Cleaner local caching of Tex/Text data, and partially cleaned up configuration (#2259)
* Remove print("Reloading...") * Change where exception mode is set, to be quieter * Add default fallback monitor for when no monitors are detected * Have StringMobject work with svg strings rather than necessarily writing to file Change SVGMobject to allow taking in a string of svg code as an input * Add caching functionality, and have Tex and Text both use it for saved svg strings * Clean up tex_file_writing * Get rid of get_tex_dir and get_text_dir * Allow for a configurable cache location * Make caching on disk a decorator, and update implementations for Tex and Text mobjects * Remove stray prints * Clean up how configuration is handled In principle, all we need here is that manim looks to the default_config.yaml file, and updates it based on any local configuration files, whether in the current working directory or as specified by a CLI argument. * Make the default size for hash_string an option * Remove utils/customization.py * Remove stray prints * Consolidate camera configuration This is still not optimal, but at least makes clearer the way that importing from constants.py kicks off some of the configuration code. * Factor out configuration to be passed into a scene vs. that used to run a scene * Use newer extract_scene.main interface * Add clarifying message to note what exactly is being reloaded * Minor clean up * Minor clean up * If it's worth caching to disk, then might as well do so in memory too during development * No longer any need for custom hash_seeds in Tex and Text * Remove display_during_execution * Get rid of (no longer used) mobject_data directory reference * Remove get_downloads_dir reference from register_font * Update where downloads go * Easier use of subdirectories in configuration * Add new pip requirements
This commit is contained in:
parent
5a70d67b98
commit
94f6f0aa96
23 changed files with 417 additions and 462 deletions
|
@ -61,9 +61,9 @@ from manimlib.scene.interactive_scene import *
|
|||
from manimlib.scene.scene import *
|
||||
|
||||
from manimlib.utils.bezier import *
|
||||
from manimlib.utils.cache import *
|
||||
from manimlib.utils.color import *
|
||||
from manimlib.utils.dict_ops import *
|
||||
from manimlib.utils.customization import *
|
||||
from manimlib.utils.debug import *
|
||||
from manimlib.utils.directories import *
|
||||
from manimlib.utils.file_ops import *
|
||||
|
|
|
@ -10,6 +10,8 @@ import screeninfo
|
|||
import sys
|
||||
import yaml
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from manimlib.logger import log
|
||||
from manimlib.utils.dict_ops import merge_dicts_recursively
|
||||
from manimlib.utils.init_config import init_customization
|
||||
|
@ -17,9 +19,12 @@ from manimlib.utils.init_config import init_customization
|
|||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
Module = importlib.util.types.ModuleType
|
||||
from typing import Optional
|
||||
|
||||
|
||||
__config_file__ = "custom_config.yml"
|
||||
# This has to be here instead of in constants.py
|
||||
# due to its use in creating the camera configuration
|
||||
FRAME_HEIGHT: float = 8.0
|
||||
|
||||
|
||||
def parse_cli():
|
||||
|
@ -300,69 +305,30 @@ def get_scene_module(args: Namespace) -> Module:
|
|||
)
|
||||
|
||||
|
||||
def get_custom_config():
|
||||
global __config_file__
|
||||
def load_yaml(file_path: str):
|
||||
try:
|
||||
with open(file_path, "r") as file:
|
||||
return yaml.safe_load(file) or {}
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_global_config():
|
||||
args = parse_cli()
|
||||
global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
|
||||
config = merge_dicts_recursively(
|
||||
load_yaml(global_defaults_file),
|
||||
load_yaml("custom_config.yml"), # From current working directory
|
||||
load_yaml(args.config_file) if args.config_file else {},
|
||||
)
|
||||
|
||||
if os.path.exists(global_defaults_file):
|
||||
with open(global_defaults_file, "r") as file:
|
||||
custom_config = yaml.safe_load(file)
|
||||
# Set the subdirectories
|
||||
base = config['directories']['base']
|
||||
for key, subdir in config['directories']['subdirs'].items():
|
||||
config['directories'][key] = os.path.join(base, subdir)
|
||||
|
||||
if os.path.exists(__config_file__):
|
||||
with open(__config_file__, "r") as file:
|
||||
local_defaults = yaml.safe_load(file)
|
||||
if local_defaults:
|
||||
custom_config = merge_dicts_recursively(
|
||||
custom_config,
|
||||
local_defaults,
|
||||
)
|
||||
else:
|
||||
with open(__config_file__, "r") as file:
|
||||
custom_config = yaml.safe_load(file)
|
||||
|
||||
# Check temporary storage(custom_config)
|
||||
if custom_config["directories"]["temporary_storage"] == "" and sys.platform == "win32":
|
||||
log.warning(
|
||||
"You may be using Windows platform and have not specified the path of" + \
|
||||
" `temporary_storage`, which may cause OSError. So it is recommended" + \
|
||||
" to specify the `temporary_storage` in the config file (.yml)"
|
||||
)
|
||||
|
||||
return custom_config
|
||||
|
||||
|
||||
def init_global_config(config_file):
|
||||
global __config_file__
|
||||
|
||||
# ensure __config_file__ always exists
|
||||
if config_file is not None:
|
||||
if not os.path.exists(config_file):
|
||||
log.error(f"Can't find {config_file}.")
|
||||
if sys.platform == 'win32':
|
||||
log.info(f"Copying default configuration file to {config_file}...")
|
||||
os.system(f"copy default_config.yml {config_file}")
|
||||
elif sys.platform in ["linux2", "darwin"]:
|
||||
log.info(f"Copying default configuration file to {config_file}...")
|
||||
os.system(f"cp default_config.yml {config_file}")
|
||||
else:
|
||||
log.info("Please create the configuration file manually.")
|
||||
log.info("Read configuration from default_config.yml.")
|
||||
else:
|
||||
__config_file__ = config_file
|
||||
|
||||
global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
|
||||
|
||||
if not (os.path.exists(global_defaults_file) or os.path.exists(__config_file__)):
|
||||
log.info("There is no configuration file detected. Switch to the config file initializer:")
|
||||
init_customization()
|
||||
|
||||
elif not os.path.exists(__config_file__):
|
||||
log.info(f"Using the default configuration file, which you can modify in `{global_defaults_file}`")
|
||||
log.info(
|
||||
"If you want to create a local configuration file, you can create a file named" + \
|
||||
f" `{__config_file__}`, or run `manimgl --config`"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def get_file_ext(args: Namespace) -> str:
|
||||
|
@ -435,7 +401,8 @@ def get_window_config(args: Namespace, custom_config: dict, camera_config: dict)
|
|||
try:
|
||||
monitors = screeninfo.get_monitors()
|
||||
except screeninfo.ScreenInfoError:
|
||||
pass
|
||||
# Default fallback
|
||||
monitors = [screeninfo.Monitor(width=1920, height=1080)]
|
||||
mon_index = custom_config["window_monitor"]
|
||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||
aspect_ratio = camera_config["pixel_width"] / camera_config["pixel_height"]
|
||||
|
@ -446,8 +413,13 @@ def get_window_config(args: Namespace, custom_config: dict, camera_config: dict)
|
|||
return dict(size=(window_width, window_height))
|
||||
|
||||
|
||||
def get_camera_config(args: Namespace, custom_config: dict) -> dict:
|
||||
camera_config = {}
|
||||
def get_camera_config(args: Optional[Namespace] = None, custom_config: Optional[dict] = None) -> dict:
|
||||
if args is None:
|
||||
args = parse_cli()
|
||||
if custom_config is None:
|
||||
custom_config = get_global_config()
|
||||
|
||||
camera_config = dict()
|
||||
camera_resolutions = custom_config["camera_resolutions"]
|
||||
if args.resolution:
|
||||
resolution = args.resolution
|
||||
|
@ -475,7 +447,7 @@ def get_camera_config(args: Namespace, custom_config: dict) -> dict:
|
|||
"pixel_width": width,
|
||||
"pixel_height": height,
|
||||
"frame_config": {
|
||||
"frame_shape": ((width / height) * get_frame_height(), get_frame_height()),
|
||||
"frame_shape": ((width / height) * FRAME_HEIGHT, FRAME_HEIGHT),
|
||||
},
|
||||
"fps": fps,
|
||||
})
|
||||
|
@ -496,21 +468,19 @@ def get_camera_config(args: Namespace, custom_config: dict) -> dict:
|
|||
return camera_config
|
||||
|
||||
|
||||
def get_configuration(args: Namespace) -> dict:
|
||||
init_global_config(args.config_file)
|
||||
custom_config = get_custom_config()
|
||||
camera_config = get_camera_config(args, custom_config)
|
||||
window_config = get_window_config(args, custom_config, camera_config)
|
||||
def get_scene_config(args: Namespace) -> dict:
|
||||
"""
|
||||
Returns a dictionary to be used as key word arguments for Scene
|
||||
"""
|
||||
global_config = get_global_config()
|
||||
camera_config = get_camera_config(args, global_config)
|
||||
window_config = get_window_config(args, global_config, camera_config)
|
||||
start, end = get_animations_numbers(args)
|
||||
|
||||
return {
|
||||
"module": get_scene_module(args),
|
||||
"scene_names": args.scene_names,
|
||||
"file_writer_config": get_file_writer_config(args, custom_config),
|
||||
"file_writer_config": get_file_writer_config(args, global_config),
|
||||
"camera_config": camera_config,
|
||||
"window_config": window_config,
|
||||
"quiet": args.quiet or args.write_all,
|
||||
"write_all": args.write_all,
|
||||
"skip_animations": args.skip_animations,
|
||||
"start_at_animation_number": start,
|
||||
"end_at_animation_number": end,
|
||||
|
@ -518,26 +488,16 @@ def get_configuration(args: Namespace) -> dict:
|
|||
"presenter_mode": args.presenter_mode,
|
||||
"leave_progress_bars": args.leave_progress_bars,
|
||||
"show_animation_progress": args.show_animation_progress,
|
||||
"prerun": args.prerun,
|
||||
"embed_exception_mode": custom_config["embed_exception_mode"],
|
||||
"embed_error_sound": custom_config["embed_error_sound"],
|
||||
"embed_exception_mode": global_config["embed_exception_mode"],
|
||||
"embed_error_sound": global_config["embed_error_sound"],
|
||||
}
|
||||
|
||||
|
||||
def get_frame_height():
|
||||
return 8.0
|
||||
|
||||
|
||||
def get_aspect_ratio():
|
||||
cam_config = get_camera_config(parse_cli(), get_custom_config())
|
||||
return cam_config['pixel_width'] / cam_config['pixel_height']
|
||||
|
||||
|
||||
def get_default_pixel_width():
|
||||
cam_config = get_camera_config(parse_cli(), get_custom_config())
|
||||
return cam_config['pixel_width']
|
||||
|
||||
|
||||
def get_default_pixel_height():
|
||||
cam_config = get_camera_config(parse_cli(), get_custom_config())
|
||||
return cam_config['pixel_height']
|
||||
def get_run_config(args: Namespace):
|
||||
return {
|
||||
"module": get_scene_module(args),
|
||||
"prerun": args.prerun,
|
||||
"scene_names": args.scene_names,
|
||||
"quiet": args.quiet or args.write_all,
|
||||
"write_all": args.write_all,
|
||||
}
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
from __future__ import annotations
|
||||
import numpy as np
|
||||
|
||||
from manimlib.config import get_aspect_ratio
|
||||
from manimlib.config import get_default_pixel_width
|
||||
from manimlib.config import get_default_pixel_height
|
||||
from manimlib.config import get_frame_height
|
||||
from manimlib.config import get_camera_config
|
||||
from manimlib.config import FRAME_HEIGHT
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
|
@ -12,16 +10,20 @@ if TYPE_CHECKING:
|
|||
from manimlib.typing import ManimColor, Vect3
|
||||
|
||||
|
||||
# TODO, it feels a bit unprincipled to have some global constants
|
||||
# depend on the output of this function, and for all that configuration
|
||||
# code to be run merely upon importing from this file.
|
||||
CAMERA_CONFIG = get_camera_config()
|
||||
|
||||
# Sizes relevant to default camera frame
|
||||
ASPECT_RATIO: float = get_aspect_ratio()
|
||||
FRAME_HEIGHT: float = get_frame_height()
|
||||
ASPECT_RATIO: float = CAMERA_CONFIG['pixel_width'] / CAMERA_CONFIG['pixel_height']
|
||||
FRAME_WIDTH: float = FRAME_HEIGHT * ASPECT_RATIO
|
||||
FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT)
|
||||
FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2
|
||||
FRAME_X_RADIUS: float = FRAME_WIDTH / 2
|
||||
|
||||
DEFAULT_PIXEL_HEIGHT: int = get_default_pixel_height()
|
||||
DEFAULT_PIXEL_WIDTH: int = get_default_pixel_width()
|
||||
DEFAULT_PIXEL_HEIGHT: int = CAMERA_CONFIG['pixel_height']
|
||||
DEFAULT_PIXEL_WIDTH: int = CAMERA_CONFIG['pixel_width']
|
||||
DEFAULT_FPS: int = 30
|
||||
|
||||
SMALL_BUFF: float = 0.1
|
||||
|
|
|
@ -3,19 +3,28 @@ directories:
|
|||
# to match the directory structure of the path to the
|
||||
# sourcecode generating that video
|
||||
mirror_module_path: False
|
||||
# Where should manim output video and image files?
|
||||
output: ""
|
||||
# If you want to use images, manim will look to these folders to find them
|
||||
raster_images: ""
|
||||
vector_images: ""
|
||||
# If you want to use sounds, manim will look here to find it.
|
||||
sounds: ""
|
||||
# Manim often generates tex_files or other kinds of serialized data
|
||||
# to keep from having to generate the same thing too many times. By
|
||||
# default, these will be stored at tempfile.gettempdir(), e.g. this might
|
||||
# return whatever is at to the TMPDIR environment variable. If you want to
|
||||
# specify them elsewhere,
|
||||
temporary_storage: ""
|
||||
# Manim may write to and read from teh file system, e.g.
|
||||
# to render videos and to look for svg/png assets. This
|
||||
# will specify where those assets live, with a base directory,
|
||||
# and various subdirectory names within it
|
||||
base: ""
|
||||
subdirs:
|
||||
# Where should manim output video and image files?
|
||||
output: "videos"
|
||||
# If you want to use images, manim will look to these folders to find them
|
||||
raster_images: "raster_images"
|
||||
vector_images: "vector_images"
|
||||
# If you want to use sounds, manim will look here to find it.
|
||||
sounds: "sounds"
|
||||
# Place for other forms of data relevant to any projects, like csv's
|
||||
data: "data"
|
||||
# When downloading, say an image, where will it go?
|
||||
downloads: "downloads"
|
||||
# For certain object types, especially Tex and Text, manim will save information
|
||||
# to file to prevent the need to re-compute, e.g. recompiling the latex. By default,
|
||||
# it stores this saved data to whatever directory appdirs.user_cache_dir("manim") returns,
|
||||
# but here a user can specify a different cache location
|
||||
cache: ""
|
||||
universal_import_line: "from manimlib import *"
|
||||
style:
|
||||
tex_template: "default"
|
||||
|
|
|
@ -2,7 +2,7 @@ import copy
|
|||
import inspect
|
||||
import sys
|
||||
|
||||
from manimlib.config import get_custom_config
|
||||
from manimlib.config import get_global_config
|
||||
from manimlib.logger import log
|
||||
from manimlib.scene.interactive_scene import InteractiveScene
|
||||
from manimlib.scene.scene import Scene
|
||||
|
@ -10,7 +10,7 @@ from manimlib.scene.scene import Scene
|
|||
|
||||
class BlankScene(InteractiveScene):
|
||||
def construct(self):
|
||||
exec(get_custom_config()["universal_import_line"])
|
||||
exec(get_global_config()["universal_import_line"])
|
||||
self.embed()
|
||||
|
||||
|
||||
|
@ -53,14 +53,6 @@ def prompt_user_for_choice(scene_classes):
|
|||
sys.exit(1)
|
||||
|
||||
|
||||
def get_scene_config(config):
|
||||
scene_parameters = inspect.signature(Scene).parameters.keys()
|
||||
return {
|
||||
key: config[key]
|
||||
for key in set(scene_parameters).intersection(config.keys())
|
||||
}
|
||||
|
||||
|
||||
def compute_total_frames(scene_class, scene_config):
|
||||
"""
|
||||
When a scene is being written to file, a copy of the scene is run with
|
||||
|
@ -79,19 +71,19 @@ def compute_total_frames(scene_class, scene_config):
|
|||
return int(total_time * scene_config["camera_config"]["fps"])
|
||||
|
||||
|
||||
def scene_from_class(scene_class, scene_config, config):
|
||||
def scene_from_class(scene_class, scene_config, run_config):
|
||||
fw_config = scene_config["file_writer_config"]
|
||||
if fw_config["write_to_movie"] and config["prerun"]:
|
||||
if fw_config["write_to_movie"] and run_config["prerun"]:
|
||||
fw_config["total_frames"] = compute_total_frames(scene_class, scene_config)
|
||||
return scene_class(**scene_config)
|
||||
|
||||
|
||||
def get_scenes_to_render(all_scene_classes, scene_config, config):
|
||||
if config["write_all"]:
|
||||
def get_scenes_to_render(all_scene_classes, scene_config, run_config):
|
||||
if run_config["write_all"]:
|
||||
return [sc(**scene_config) for sc in all_scene_classes]
|
||||
|
||||
names_to_classes = {sc.__name__ : sc for sc in all_scene_classes}
|
||||
scene_names = config["scene_names"]
|
||||
names_to_classes = {sc.__name__: sc for sc in all_scene_classes}
|
||||
scene_names = run_config["scene_names"]
|
||||
|
||||
for name in set.difference(set(scene_names), names_to_classes):
|
||||
log.error(f"No scene named {name} found")
|
||||
|
@ -105,7 +97,7 @@ def get_scenes_to_render(all_scene_classes, scene_config, config):
|
|||
classes_to_run = prompt_user_for_choice(all_scene_classes)
|
||||
|
||||
return [
|
||||
scene_from_class(scene_class, scene_config, config)
|
||||
scene_from_class(scene_class, scene_config, run_config)
|
||||
for scene_class in classes_to_run
|
||||
]
|
||||
|
||||
|
@ -123,13 +115,10 @@ def get_scene_classes_from_module(module):
|
|||
]
|
||||
|
||||
|
||||
def main(config):
|
||||
module = config["module"]
|
||||
scene_config = get_scene_config(config)
|
||||
if module is None:
|
||||
def main(scene_config, run_config):
|
||||
if run_config["module"] is None:
|
||||
# If no module was passed in, just play the blank scene
|
||||
return [BlankScene(**scene_config)]
|
||||
|
||||
all_scene_classes = get_scene_classes_from_module(module)
|
||||
scenes = get_scenes_to_render(all_scene_classes, scene_config, config)
|
||||
return scenes
|
||||
all_scene_classes = get_scene_classes_from_module(run_config["module"])
|
||||
return get_scenes_to_render(all_scene_classes, scene_config, run_config)
|
||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from colour import Color
|
||||
|
||||
from manimlib.config import get_global_config
|
||||
from manimlib.constants import BLACK, RED, YELLOW, WHITE
|
||||
from manimlib.constants import DL, DOWN, DR, LEFT, RIGHT, UL, UR
|
||||
from manimlib.constants import SMALL_BUFF
|
||||
|
@ -9,7 +10,6 @@ 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.customization import get_customization
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
@ -57,7 +57,7 @@ class BackgroundRectangle(SurroundingRectangle):
|
|||
**kwargs
|
||||
):
|
||||
if color is None:
|
||||
color = get_customization()['style']['background_color']
|
||||
color = get_global_config()['style']['background_color']
|
||||
super().__init__(
|
||||
mobject,
|
||||
color=color,
|
||||
|
|
|
@ -7,7 +7,7 @@ import re
|
|||
from manimlib.constants import BLACK, WHITE
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.tex_file_writing import tex_content_to_svg_file
|
||||
from manimlib.utils.tex_file_writing import latex_to_svg
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
@ -76,12 +76,8 @@ class SingleStringTex(SVGMobject):
|
|||
self.additional_preamble
|
||||
)
|
||||
|
||||
def get_file_path(self) -> str:
|
||||
content = self.get_tex_file_body(self.tex_string)
|
||||
file_path = tex_content_to_svg_file(
|
||||
content, self.template, self.additional_preamble, self.tex_string
|
||||
)
|
||||
return file_path
|
||||
def get_svg_string_by_content(self, content: str) -> str:
|
||||
return latex_to_svg(content, self.template, self.additional_preamble)
|
||||
|
||||
def get_tex_file_body(self, tex_string: str) -> str:
|
||||
new_tex = self.get_modified_expression(tex_string)
|
||||
|
|
|
@ -66,17 +66,18 @@ class StringMobject(SVGMobject, ABC):
|
|||
self.use_labelled_svg = use_labelled_svg
|
||||
|
||||
self.parse()
|
||||
super().__init__(**kwargs)
|
||||
svg_string = self.get_svg_string()
|
||||
super().__init__(svg_string=svg_string, **kwargs)
|
||||
self.set_stroke(stroke_color, stroke_width)
|
||||
self.set_fill(fill_color, border_width=fill_border_width)
|
||||
self.labels = [submob.label for submob in self.submobjects]
|
||||
|
||||
def get_file_path(self, is_labelled: bool = False) -> str:
|
||||
is_labelled = is_labelled or self.use_labelled_svg
|
||||
return self.get_file_path_by_content(self.get_content(is_labelled))
|
||||
def get_svg_string(self, is_labelled: bool = False) -> str:
|
||||
content = self.get_content(is_labelled or self.use_labelled_svg)
|
||||
return self.get_svg_string_by_content(content)
|
||||
|
||||
@abstractmethod
|
||||
def get_file_path_by_content(self, content: str) -> str:
|
||||
def get_svg_string_by_content(self, content: str) -> str:
|
||||
return ""
|
||||
|
||||
def assign_labels_by_color(self, mobjects: list[VMobject]) -> None:
|
||||
|
@ -109,8 +110,8 @@ class StringMobject(SVGMobject, ABC):
|
|||
)
|
||||
)
|
||||
|
||||
def mobjects_from_file(self, file_path: str) -> list[VMobject]:
|
||||
submobs = super().mobjects_from_file(file_path)
|
||||
def mobjects_from_svg_string(self, svg_string: str) -> list[VMobject]:
|
||||
submobs = super().mobjects_from_svg_string(svg_string)
|
||||
|
||||
if self.use_labelled_svg:
|
||||
# This means submobjects are colored according to spans
|
||||
|
|
|
@ -5,6 +5,7 @@ from xml.etree import ElementTree as ET
|
|||
import numpy as np
|
||||
import svgelements as se
|
||||
import io
|
||||
from pathlib import Path
|
||||
|
||||
from manimlib.constants import RIGHT
|
||||
from manimlib.logger import log
|
||||
|
@ -39,6 +40,7 @@ class SVGMobject(VMobject):
|
|||
def __init__(
|
||||
self,
|
||||
file_name: str = "",
|
||||
svg_string: str = "",
|
||||
should_center: bool = True,
|
||||
height: float | None = None,
|
||||
width: float | None = None,
|
||||
|
@ -63,11 +65,19 @@ class SVGMobject(VMobject):
|
|||
path_string_config: dict = dict(),
|
||||
**kwargs
|
||||
):
|
||||
self.file_name = file_name or self.file_name
|
||||
if svg_string != "":
|
||||
self.svg_string = svg_string
|
||||
elif file_name != "":
|
||||
self.svg_string = self.file_name_to_svg_string(file_name)
|
||||
elif self.file_name != "":
|
||||
self.file_name_to_svg_string(self.file_name)
|
||||
else:
|
||||
raise Exception("Must specify either a file_name or svg_string SVGMobject")
|
||||
|
||||
self.svg_default = dict(svg_default)
|
||||
self.path_string_config = dict(path_string_config)
|
||||
|
||||
super().__init__(**kwargs )
|
||||
super().__init__(**kwargs)
|
||||
self.init_svg_mobject()
|
||||
self.ensure_positive_orientation()
|
||||
|
||||
|
@ -97,7 +107,7 @@ class SVGMobject(VMobject):
|
|||
if hash_val in SVG_HASH_TO_MOB_MAP:
|
||||
submobs = [sm.copy() for sm in SVG_HASH_TO_MOB_MAP[hash_val]]
|
||||
else:
|
||||
submobs = self.mobjects_from_file(self.get_file_path())
|
||||
submobs = self.mobjects_from_svg_string(self.svg_string)
|
||||
SVG_HASH_TO_MOB_MAP[hash_val] = [sm.copy() for sm in submobs]
|
||||
|
||||
self.add(*submobs)
|
||||
|
@ -111,11 +121,11 @@ class SVGMobject(VMobject):
|
|||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.file_name
|
||||
self.svg_string
|
||||
)
|
||||
|
||||
def mobjects_from_file(self, file_path: str) -> list[VMobject]:
|
||||
element_tree = ET.parse(file_path)
|
||||
def mobjects_from_svg_string(self, svg_string: str) -> list[VMobject]:
|
||||
element_tree = ET.ElementTree(ET.fromstring(svg_string))
|
||||
new_tree = self.modify_xml_tree(element_tree)
|
||||
|
||||
# New svg based on tree contents
|
||||
|
@ -127,10 +137,8 @@ class SVGMobject(VMobject):
|
|||
|
||||
return self.mobjects_from_svg(svg)
|
||||
|
||||
def get_file_path(self) -> str:
|
||||
if self.file_name is None:
|
||||
raise Exception("Must specify file for SVGMobject")
|
||||
return get_full_vector_image_path(self.file_name)
|
||||
def file_name_to_svg_string(self, file_name: str) -> str:
|
||||
return Path(get_full_vector_image_path(file_name)).read_text()
|
||||
|
||||
def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree:
|
||||
config_style_attrs = self.generate_config_style_dict()
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from manimlib.mobject.svg.string_mobject import StringMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.color import color_to_hex
|
||||
from manimlib.utils.color import hex_to_int
|
||||
from manimlib.utils.tex_file_writing import tex_content_to_svg_file
|
||||
from manimlib.utils.tex_file_writing import latex_to_svg
|
||||
from manimlib.utils.tex import num_tex_symbols
|
||||
from manimlib.utils.simple_functions import hash_string
|
||||
from manimlib.logger import log
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
@ -65,27 +67,8 @@ class Tex(StringMobject):
|
|||
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
|
||||
self.scale(SCALE_FACTOR_PER_FONT_POINT * font_size)
|
||||
|
||||
@property
|
||||
def hash_seed(self) -> tuple:
|
||||
return (
|
||||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.base_color,
|
||||
self.isolate,
|
||||
self.protect,
|
||||
self.tex_string,
|
||||
self.alignment,
|
||||
self.tex_environment,
|
||||
self.tex_to_color_map,
|
||||
self.template,
|
||||
self.additional_preamble
|
||||
)
|
||||
|
||||
def get_file_path_by_content(self, content: str) -> str:
|
||||
return tex_content_to_svg_file(
|
||||
content, self.template, self.additional_preamble, self.tex_string
|
||||
)
|
||||
def get_svg_string_by_content(self, content: str) -> str:
|
||||
return latex_to_svg(content, self.template, self.additional_preamble, short_tex=self.tex_string)
|
||||
|
||||
def _handle_scale_side_effects(self, scale_factor: float) -> Self:
|
||||
self.font_size *= scale_factor
|
||||
|
|
|
@ -4,21 +4,23 @@ from contextlib import contextmanager
|
|||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tempfile
|
||||
import hashlib
|
||||
from functools import lru_cache
|
||||
|
||||
import manimpango
|
||||
import pygments
|
||||
import pygments.formatters
|
||||
import pygments.lexers
|
||||
|
||||
from manimlib.config import get_global_config
|
||||
from manimlib.constants import DEFAULT_PIXEL_WIDTH, FRAME_WIDTH
|
||||
from manimlib.constants import NORMAL
|
||||
from manimlib.logger import log
|
||||
from manimlib.mobject.svg.string_mobject import StringMobject
|
||||
from manimlib.utils.customization import get_customization
|
||||
from manimlib.utils.cache import cache_on_disk
|
||||
from manimlib.utils.color import color_to_hex
|
||||
from manimlib.utils.color import int_to_hex
|
||||
from manimlib.utils.directories import get_downloads_dir
|
||||
from manimlib.utils.directories import get_text_dir
|
||||
from manimlib.utils.simple_functions import hash_string
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
@ -49,6 +51,58 @@ class _Alignment:
|
|||
self.value = _Alignment.VAL_DICT[s.upper()]
|
||||
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
@cache_on_disk
|
||||
def markup_to_svg(
|
||||
markup_str: str,
|
||||
justify: bool = False,
|
||||
indent: float = 0,
|
||||
alignment: str = "CENTER",
|
||||
line_width: float | None = None,
|
||||
) -> str:
|
||||
validate_error = manimpango.MarkupUtils.validate(markup_str)
|
||||
if validate_error:
|
||||
raise ValueError(
|
||||
f"Invalid markup string \"{markup_str}\"\n" + \
|
||||
f"{validate_error}"
|
||||
)
|
||||
|
||||
# `manimpango` is under construction,
|
||||
# so the following code is intended to suit its interface
|
||||
alignment = _Alignment(alignment)
|
||||
if line_width is None:
|
||||
pango_width = -1
|
||||
else:
|
||||
pango_width = line_width / FRAME_WIDTH * DEFAULT_PIXEL_WIDTH
|
||||
|
||||
# Write the result to a temporary svg file, and return it's contents.
|
||||
# TODO, better would be to have this not write to file at all
|
||||
with tempfile.NamedTemporaryFile(suffix='.svg', mode='r+') as tmp:
|
||||
manimpango.MarkupUtils.text2svg(
|
||||
text=markup_str,
|
||||
font="", # Already handled
|
||||
slant="NORMAL", # Already handled
|
||||
weight="NORMAL", # Already handled
|
||||
size=1, # Already handled
|
||||
_=0, # Empty parameter
|
||||
disable_liga=False,
|
||||
file_name=tmp.name,
|
||||
START_X=0,
|
||||
START_Y=0,
|
||||
width=DEFAULT_CANVAS_WIDTH,
|
||||
height=DEFAULT_CANVAS_HEIGHT,
|
||||
justify=justify,
|
||||
indent=indent,
|
||||
line_spacing=None, # Already handled
|
||||
alignment=alignment,
|
||||
pango_width=pango_width
|
||||
)
|
||||
|
||||
# Read the contents
|
||||
tmp.seek(0)
|
||||
return tmp.read()
|
||||
|
||||
|
||||
class MarkupText(StringMobject):
|
||||
# See https://docs.gtk.org/Pango/pango_markup.html
|
||||
MARKUP_TAGS = {
|
||||
|
@ -102,13 +156,14 @@ class MarkupText(StringMobject):
|
|||
isolate: Selector = re.compile(r"\w+", re.U),
|
||||
**kwargs
|
||||
):
|
||||
default_style = get_global_config()["style"]
|
||||
self.text = text
|
||||
self.font_size = font_size
|
||||
self.justify = justify
|
||||
self.indent = indent
|
||||
self.alignment = alignment or get_customization()["style"]["text_alignment"]
|
||||
self.alignment = alignment or default_style["text_alignment"]
|
||||
self.line_width = line_width
|
||||
self.font = font or get_customization()["style"]["font"]
|
||||
self.font = font or default_style["font"]
|
||||
self.slant = slant
|
||||
self.weight = weight
|
||||
|
||||
|
@ -141,88 +196,14 @@ class MarkupText(StringMobject):
|
|||
if height is None:
|
||||
self.scale(TEXT_MOB_SCALE_FACTOR)
|
||||
|
||||
@property
|
||||
def hash_seed(self) -> tuple:
|
||||
return (
|
||||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.base_color,
|
||||
self.isolate,
|
||||
self.protect,
|
||||
self.text,
|
||||
self.font_size,
|
||||
self.lsh,
|
||||
self.justify,
|
||||
self.indent,
|
||||
self.alignment,
|
||||
self.line_width,
|
||||
self.font,
|
||||
self.slant,
|
||||
self.weight,
|
||||
self.t2c,
|
||||
self.t2f,
|
||||
self.t2s,
|
||||
self.t2w,
|
||||
self.global_config,
|
||||
self.local_configs,
|
||||
self.disable_ligatures
|
||||
)
|
||||
|
||||
def get_file_path_by_content(self, content: str) -> str:
|
||||
hash_content = str((
|
||||
def get_svg_string_by_content(self, content: str) -> str:
|
||||
self.content = content
|
||||
return markup_to_svg(
|
||||
content,
|
||||
self.justify,
|
||||
self.indent,
|
||||
self.alignment,
|
||||
self.line_width
|
||||
))
|
||||
svg_file = os.path.join(
|
||||
get_text_dir(), hash_string(hash_content) + ".svg"
|
||||
)
|
||||
if not os.path.exists(svg_file):
|
||||
self.markup_to_svg(content, svg_file)
|
||||
return svg_file
|
||||
|
||||
def markup_to_svg(self, markup_str: str, file_name: str) -> str:
|
||||
self.validate_markup_string(markup_str)
|
||||
|
||||
# `manimpango` is under construction,
|
||||
# so the following code is intended to suit its interface
|
||||
alignment = _Alignment(self.alignment)
|
||||
if self.line_width is None:
|
||||
pango_width = -1
|
||||
else:
|
||||
pango_width = self.line_width / FRAME_WIDTH * DEFAULT_PIXEL_WIDTH
|
||||
|
||||
return manimpango.MarkupUtils.text2svg(
|
||||
text=markup_str,
|
||||
font="", # Already handled
|
||||
slant="NORMAL", # Already handled
|
||||
weight="NORMAL", # Already handled
|
||||
size=1, # Already handled
|
||||
_=0, # Empty parameter
|
||||
disable_liga=False,
|
||||
file_name=file_name,
|
||||
START_X=0,
|
||||
START_Y=0,
|
||||
width=DEFAULT_CANVAS_WIDTH,
|
||||
height=DEFAULT_CANVAS_HEIGHT,
|
||||
justify=self.justify,
|
||||
indent=self.indent,
|
||||
line_spacing=None, # Already handled
|
||||
alignment=alignment,
|
||||
pango_width=pango_width
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_markup_string(markup_str: str) -> None:
|
||||
validate_error = manimpango.MarkupUtils.validate(markup_str)
|
||||
if not validate_error:
|
||||
return
|
||||
raise ValueError(
|
||||
f"Invalid markup string \"{markup_str}\"\n" + \
|
||||
f"{validate_error}"
|
||||
alignment=self.alignment,
|
||||
line_width=self.line_width
|
||||
)
|
||||
|
||||
# Toolkits
|
||||
|
@ -511,20 +492,10 @@ def register_font(font_file: str | Path):
|
|||
method with previous releases will raise an :class:`AttributeError` on macOS.
|
||||
"""
|
||||
|
||||
input_folder = Path(get_downloads_dir()).parent.resolve()
|
||||
possible_paths = [
|
||||
Path(font_file),
|
||||
input_folder / font_file,
|
||||
]
|
||||
for path in possible_paths:
|
||||
path = path.resolve()
|
||||
if path.exists():
|
||||
file_path = path
|
||||
break
|
||||
else:
|
||||
error = f"Can't find {font_file}." f"Tried these : {possible_paths}"
|
||||
file_path = Path(font_file).resolve()
|
||||
if not file_path.exists():
|
||||
error = f"Can't find {font_file}."
|
||||
raise FileNotFoundError(error)
|
||||
|
||||
try:
|
||||
assert manimpango.register_font(str(file_path))
|
||||
yield
|
||||
|
|
|
@ -37,8 +37,6 @@ class ReloadManager:
|
|||
except KillEmbedded:
|
||||
# Requested via the `exit_raise` IPython runline magic
|
||||
# by means of our scene.reload() command
|
||||
print("Reloading...")
|
||||
|
||||
for scene in self.scenes:
|
||||
scene.tear_down()
|
||||
|
||||
|
@ -61,12 +59,14 @@ class ReloadManager:
|
|||
self.args.embed = str(overwrite_start_at_line)
|
||||
|
||||
# Args to Config
|
||||
config = manimlib.config.get_configuration(self.args)
|
||||
scene_config = manimlib.config.get_scene_config(self.args)
|
||||
if self.window:
|
||||
config["existing_window"] = self.window # see scene initialization
|
||||
scene_config["existing_window"] = self.window # see scene initialization
|
||||
|
||||
run_config = manimlib.config.get_run_config(self.args)
|
||||
|
||||
# Scenes
|
||||
self.scenes = manimlib.extract_scene.main(config)
|
||||
self.scenes = manimlib.extract_scene.main(scene_config, run_config)
|
||||
if len(self.scenes) == 0:
|
||||
print("No scenes found to run")
|
||||
return
|
||||
|
@ -78,7 +78,13 @@ class ReloadManager:
|
|||
break
|
||||
|
||||
for scene in self.scenes:
|
||||
if self.args.embed:
|
||||
print(" ".join([
|
||||
"Loading interactive session for",
|
||||
f"\033[96m{self.args.scene_names[0]}\033[0m",
|
||||
f"in \033[96m{self.args.file}\033[0m",
|
||||
f"at line \033[96m{self.args.embed}\033[0m"
|
||||
]))
|
||||
scene.run()
|
||||
|
||||
|
||||
reload_manager = ReloadManager()
|
||||
|
|
|
@ -232,7 +232,11 @@ class Scene(object):
|
|||
# the local namespace of the caller
|
||||
caller_frame = inspect.currentframe().f_back
|
||||
module = get_module(caller_frame.f_globals["__file__"])
|
||||
shell = InteractiveShellEmbed(user_module=module)
|
||||
shell = InteractiveShellEmbed(
|
||||
user_module=module,
|
||||
display_banner=False,
|
||||
xmode=self.embed_exception_mode
|
||||
)
|
||||
self.shell = shell
|
||||
|
||||
# Add a few custom shortcuts to that local namespace
|
||||
|
@ -288,9 +292,6 @@ class Scene(object):
|
|||
|
||||
shell.set_custom_exc((Exception,), custom_exc)
|
||||
|
||||
# Set desired exception mode
|
||||
shell.magic(f"xmode {self.embed_exception_mode}")
|
||||
|
||||
# Launch shell
|
||||
shell()
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import numpy as np
|
|||
from functools import lru_cache
|
||||
|
||||
from manimlib.config import parse_cli
|
||||
from manimlib.config import get_configuration
|
||||
from manimlib.config import get_camera_config
|
||||
from manimlib.utils.shaders import get_shader_code_from_file
|
||||
from manimlib.utils.shaders import get_shader_program
|
||||
from manimlib.utils.shaders import image_path_to_texture
|
||||
|
@ -410,7 +410,7 @@ class VShaderWrapper(ShaderWrapper):
|
|||
which can display that texture as a simple quad onto a screen,
|
||||
along with the rgb value which is meant to be discarded.
|
||||
"""
|
||||
cam_config = get_configuration(parse_cli())['camera_config']
|
||||
cam_config = get_camera_config()
|
||||
size = (cam_config['pixel_width'], cam_config['pixel_height'])
|
||||
double_size = (2 * size[0], 2 * size[1])
|
||||
|
||||
|
|
34
manimlib/utils/cache.py
Normal file
34
manimlib/utils/cache.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from diskcache import Cache
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
|
||||
from manimlib.utils.directories import get_cache_dir
|
||||
from manimlib.utils.simple_functions import hash_string
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
CACHE_SIZE = 1e9 # 1 Gig
|
||||
_cache = Cache(get_cache_dir(), size_limit=CACHE_SIZE)
|
||||
|
||||
|
||||
def cache_on_disk(func: Callable[..., T]) -> Callable[..., T]:
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
key = hash_string(f"{func.__name__}{args}{kwargs}")
|
||||
value = _cache.get(key)
|
||||
if value is None:
|
||||
value = func(*args, **kwargs)
|
||||
_cache.set(key, value)
|
||||
return value
|
||||
return wrapper
|
||||
|
||||
|
||||
def clear_cache():
|
||||
_cache.clear()
|
|
@ -1,24 +0,0 @@
|
|||
import os
|
||||
import tempfile
|
||||
|
||||
from manimlib.config import get_custom_config
|
||||
from manimlib.config import get_manim_dir
|
||||
|
||||
|
||||
CUSTOMIZATION = {}
|
||||
|
||||
|
||||
def get_customization():
|
||||
if not CUSTOMIZATION:
|
||||
CUSTOMIZATION.update(get_custom_config())
|
||||
directories = CUSTOMIZATION["directories"]
|
||||
# Unless user has specified otherwise, use the system default temp
|
||||
# directory for storing tex files, mobject_data, etc.
|
||||
if not directories["temporary_storage"]:
|
||||
directories["temporary_storage"] = tempfile.gettempdir()
|
||||
|
||||
# Assumes all shaders are written into manimlib/shaders
|
||||
directories["shaders"] = os.path.join(
|
||||
get_manim_dir(), "manimlib", "shaders"
|
||||
)
|
||||
return CUSTOMIZATION
|
|
@ -1,33 +1,29 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import appdirs
|
||||
|
||||
from manimlib.utils.customization import get_customization
|
||||
|
||||
from manimlib.config import get_global_config
|
||||
from manimlib.config import get_manim_dir
|
||||
from manimlib.utils.file_ops import guarantee_existence
|
||||
|
||||
|
||||
def get_directories() -> dict[str, str]:
|
||||
return get_customization()["directories"]
|
||||
return get_global_config()["directories"]
|
||||
|
||||
|
||||
def get_cache_dir() -> str:
|
||||
return get_directories()["cache"] or appdirs.user_cache_dir("manim")
|
||||
|
||||
|
||||
def get_temp_dir() -> str:
|
||||
return get_directories()["temporary_storage"]
|
||||
|
||||
|
||||
def get_tex_dir() -> str:
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "Tex"))
|
||||
|
||||
|
||||
def get_text_dir() -> str:
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "Text"))
|
||||
|
||||
|
||||
def get_mobject_data_dir() -> str:
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "mobject_data"))
|
||||
return get_directories()["temporary_storage"] or tempfile.gettempdir()
|
||||
|
||||
|
||||
def get_downloads_dir() -> str:
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "manim_downloads"))
|
||||
return get_directories()["downloads"] or appdirs.user_cache_dir("manim_downloads")
|
||||
|
||||
|
||||
def get_output_dir() -> str:
|
||||
|
@ -47,4 +43,4 @@ def get_sound_dir() -> str:
|
|||
|
||||
|
||||
def get_shader_dir() -> str:
|
||||
return get_directories()["shaders"]
|
||||
return os.path.join(get_manim_dir(), "manimlib", "shaders")
|
||||
|
|
|
@ -6,6 +6,10 @@ import hashlib
|
|||
|
||||
import numpy as np
|
||||
import validators
|
||||
import urllib.request
|
||||
|
||||
import manimlib.utils.directories
|
||||
from manimlib.utils.simple_functions import hash_string
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
@ -35,11 +39,9 @@ def find_file(
|
|||
# Check if this is a file online first, and if so, download
|
||||
# it to a temporary directory
|
||||
if validators.url(file_name):
|
||||
import urllib.request
|
||||
from manimlib.utils.directories import get_downloads_dir
|
||||
suffix = Path(file_name).suffix
|
||||
file_hash = hashlib.sha256(file_name.encode('utf-8')).hexdigest()[:32]
|
||||
folder = get_downloads_dir()
|
||||
file_hash = hash_string(file_name)
|
||||
folder = manimlib.utils.directories.get_downloads_dir()
|
||||
|
||||
path = Path(folder, file_hash).with_suffix(suffix)
|
||||
urllib.request.urlretrieve(file_name, path)
|
||||
|
|
|
@ -36,11 +36,15 @@ def init_customization() -> None:
|
|||
configuration = {
|
||||
"directories": {
|
||||
"mirror_module_path": False,
|
||||
"output": "",
|
||||
"raster_images": "",
|
||||
"vector_images": "",
|
||||
"sounds": "",
|
||||
"temporary_storage": "",
|
||||
"base": "",
|
||||
"subdirs": {
|
||||
"output": "videos",
|
||||
"raster_images": "raster_images",
|
||||
"vector_images": "vector_images",
|
||||
"sounds": "sounds",
|
||||
"data": "data",
|
||||
"downloads": "downloads",
|
||||
}
|
||||
},
|
||||
"universal_import_line": "from manimlib import *",
|
||||
"style": {
|
||||
|
@ -74,33 +78,39 @@ def init_customization() -> None:
|
|||
|
||||
console.print("[bold]Directories:[/bold]")
|
||||
dir_config = configuration["directories"]
|
||||
dir_config["output"] = Prompt.ask(
|
||||
" Where should manim [bold]output[/bold] video and image files place [prompt.default](optional, default is none)",
|
||||
dir_config["base"] = Prompt.ask(
|
||||
" What base directory should manim use for reading/writing video and images? [prompt.default](optional, default is none)",
|
||||
default="",
|
||||
show_default=False
|
||||
)
|
||||
dir_config["raster_images"] = Prompt.ask(
|
||||
" Which folder should manim find [bold]raster images[/bold] (.jpg .png .gif) in " + \
|
||||
"[prompt.default](optional, default is none)",
|
||||
default="",
|
||||
dir_config["subdirs"]["output"] = Prompt.ask(
|
||||
" Within that base directory, which subdirectory should manim [bold]output[/bold] video and image files to?" + \
|
||||
" [prompt.default](optional, default is \"videos\")",
|
||||
default="videos",
|
||||
show_default=False
|
||||
)
|
||||
dir_config["vector_images"] = Prompt.ask(
|
||||
" Which folder should manim find [bold]vector images[/bold] (.svg .xdv) in " + \
|
||||
"[prompt.default](optional, default is none)",
|
||||
default="",
|
||||
dir_config["subdirs"]["raster_images"] = Prompt.ask(
|
||||
" Within that base directory, which subdirectory should manim look for raster images (.png, .jpg)" + \
|
||||
" [prompt.default](optional, default is \"raster_images\")",
|
||||
default="raster_images",
|
||||
show_default=False
|
||||
)
|
||||
dir_config["sounds"] = Prompt.ask(
|
||||
" Which folder should manim find [bold]sound files[/bold] (.mp3 .wav) in " + \
|
||||
"[prompt.default](optional, default is none)",
|
||||
default="",
|
||||
dir_config["subdirs"]["vector_images"] = Prompt.ask(
|
||||
" Within that base directory, which subdirectory should manim look for raster images (.svg, .xdv)" + \
|
||||
" [prompt.default](optional, default is \"vector_images\")",
|
||||
default="vector_images",
|
||||
show_default=False
|
||||
)
|
||||
dir_config["temporary_storage"] = Prompt.ask(
|
||||
" Which folder should manim storage [bold]temporary files[/bold] " + \
|
||||
"[prompt.default](recommended, use system temporary folder by default)",
|
||||
default="",
|
||||
dir_config["subdirs"]["sounds"] = Prompt.ask(
|
||||
" Within that base directory, which subdirectory should manim look for sound files (.mp3, .wav)" + \
|
||||
" [prompt.default](optional, default is \"sounds\")",
|
||||
default="sounds",
|
||||
show_default=False
|
||||
)
|
||||
dir_config["subdirs"]["downloads"] = Prompt.ask(
|
||||
" Within that base directory, which subdirectory should manim output downloaded files" + \
|
||||
" [prompt.default](optional, default is \"downloads\")",
|
||||
default="downloads",
|
||||
show_default=False
|
||||
)
|
||||
|
||||
|
|
|
@ -96,7 +96,6 @@ def binary_search(function: Callable[[float], float],
|
|||
return mh
|
||||
|
||||
|
||||
def hash_string(string: str) -> str:
|
||||
# Truncating at 16 bytes for cleanliness
|
||||
def hash_string(string: str, n_bytes=16) -> str:
|
||||
hasher = hashlib.sha256(string.encode())
|
||||
return hasher.hexdigest()[:16]
|
||||
return hasher.hexdigest()[:n_bytes]
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
import subprocess
|
||||
from functools import lru_cache
|
||||
|
||||
from manimlib.config import get_custom_config
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
from manimlib.utils.cache import cache_on_disk
|
||||
from manimlib.config import get_global_config
|
||||
from manimlib.config import get_manim_dir
|
||||
from manimlib.logger import log
|
||||
from manimlib.utils.directories import get_tex_dir
|
||||
from manimlib.utils.simple_functions import hash_string
|
||||
|
||||
|
||||
|
@ -17,9 +21,8 @@ SAVED_TEX_CONFIG = {}
|
|||
|
||||
def get_tex_template_config(template_name: str) -> dict[str, str]:
|
||||
name = template_name.replace(" ", "_").lower()
|
||||
with open(os.path.join(
|
||||
get_manim_dir(), "manimlib", "tex_templates.yml"
|
||||
), encoding="utf-8") as tex_templates_file:
|
||||
template_path = os.path.join(get_manim_dir(), "manimlib", "tex_templates.yml")
|
||||
with open(template_path, encoding="utf-8") as tex_templates_file:
|
||||
templates_dict = yaml.safe_load(tex_templates_file)
|
||||
if name not in templates_dict:
|
||||
log.warning(
|
||||
|
@ -41,7 +44,7 @@ def get_tex_config() -> dict[str, str]:
|
|||
"""
|
||||
# Only load once, then save thereafter
|
||||
if not SAVED_TEX_CONFIG:
|
||||
template_name = get_custom_config()["style"]["tex_template"]
|
||||
template_name = get_global_config()["style"]["tex_template"]
|
||||
template_config = get_tex_template_config(template_name)
|
||||
SAVED_TEX_CONFIG.update({
|
||||
"template": template_name,
|
||||
|
@ -51,22 +54,8 @@ def get_tex_config() -> dict[str, str]:
|
|||
return SAVED_TEX_CONFIG
|
||||
|
||||
|
||||
def tex_content_to_svg_file(
|
||||
content: str, template: str, additional_preamble: str,
|
||||
short_tex: str
|
||||
) -> str:
|
||||
tex_config = get_tex_config()
|
||||
if not template or template == tex_config["template"]:
|
||||
compiler = tex_config["compiler"]
|
||||
preamble = tex_config["preamble"]
|
||||
else:
|
||||
config = get_tex_template_config(template)
|
||||
compiler = config["compiler"]
|
||||
preamble = config["preamble"]
|
||||
|
||||
if additional_preamble:
|
||||
preamble += "\n" + additional_preamble
|
||||
full_tex = "\n\n".join((
|
||||
def get_full_tex(content: str, preamble: str = ""):
|
||||
return "\n\n".join((
|
||||
"\\documentclass[preview]{standalone}",
|
||||
preamble,
|
||||
"\\begin{document}",
|
||||
|
@ -74,17 +63,43 @@ def tex_content_to_svg_file(
|
|||
"\\end{document}"
|
||||
)) + "\n"
|
||||
|
||||
svg_file = os.path.join(
|
||||
get_tex_dir(), hash_string(full_tex) + ".svg"
|
||||
)
|
||||
if not os.path.exists(svg_file):
|
||||
# If svg doesn't exist, create it
|
||||
with display_during_execution("Writing " + short_tex):
|
||||
create_tex_svg(full_tex, svg_file, compiler)
|
||||
return svg_file
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
@cache_on_disk
|
||||
def latex_to_svg(
|
||||
latex: str,
|
||||
template: str = "",
|
||||
additional_preamble: str = "",
|
||||
short_tex: str = "",
|
||||
show_message_during_execution: bool = True,
|
||||
) -> str:
|
||||
"""Convert LaTeX string to SVG string.
|
||||
|
||||
Args:
|
||||
latex: LaTeX source code
|
||||
template: Path to a template LaTeX file
|
||||
additional_preamble: String including any added "\\usepackage{...}" style imports
|
||||
|
||||
Returns:
|
||||
str: SVG source code
|
||||
|
||||
Raises:
|
||||
LatexError: If LaTeX compilation fails
|
||||
NotImplementedError: If compiler is not supported
|
||||
"""
|
||||
if show_message_during_execution:
|
||||
max_message_len = 80
|
||||
message = f"Writing {short_tex or latex}"
|
||||
if len(message) > max_message_len:
|
||||
message = message[:max_message_len - 3] + "..."
|
||||
print(message, end="\r")
|
||||
|
||||
tex_config = get_tex_config()
|
||||
if template and template != tex_config["template"]:
|
||||
tex_config = get_tex_template_config(template)
|
||||
|
||||
compiler = tex_config["compiler"]
|
||||
|
||||
def create_tex_svg(full_tex: str, svg_file: str, compiler: str) -> None:
|
||||
if compiler == "latex":
|
||||
program = "latex"
|
||||
dvi_ext = ".dvi"
|
||||
|
@ -92,72 +107,65 @@ def create_tex_svg(full_tex: str, svg_file: str, compiler: str) -> None:
|
|||
program = "xelatex -no-pdf"
|
||||
dvi_ext = ".xdv"
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Compiler '{compiler}' is not implemented"
|
||||
raise NotImplementedError(f"Compiler '{compiler}' is not implemented")
|
||||
|
||||
preamble = tex_config["preamble"] + "\n" + additional_preamble
|
||||
full_tex = get_full_tex(latex, preamble)
|
||||
|
||||
# Write intermediate files to a temporary directory
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
base_path = os.path.join(temp_dir, "working")
|
||||
tex_path = base_path + ".tex"
|
||||
dvi_path = base_path + dvi_ext
|
||||
|
||||
# Write tex file
|
||||
with open(tex_path, "w", encoding="utf-8") as tex_file:
|
||||
tex_file.write(full_tex)
|
||||
|
||||
# Run latex compiler
|
||||
process = subprocess.run(
|
||||
[
|
||||
program.split()[0], # Split for xelatex case
|
||||
"-interaction=batchmode",
|
||||
"-halt-on-error",
|
||||
"-output-directory=" + temp_dir,
|
||||
tex_path
|
||||
] + (["--no-pdf"] if compiler == "xelatex" else []),
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Write tex file
|
||||
root, _ = os.path.splitext(svg_file)
|
||||
with open(root + ".tex", "w", encoding="utf-8") as tex_file:
|
||||
tex_file.write(full_tex)
|
||||
if process.returncode != 0:
|
||||
# Handle error
|
||||
error_str = ""
|
||||
log_path = base_path + ".log"
|
||||
if os.path.exists(log_path):
|
||||
with open(log_path, "r", encoding="utf-8") as log_file:
|
||||
content = log_file.read()
|
||||
error_match = re.search(r"(?<=\n! ).*\n.*\n", content)
|
||||
if error_match:
|
||||
error_str = error_match.group()
|
||||
raise LatexError(error_str or "LaTeX compilation failed")
|
||||
|
||||
# tex to dvi
|
||||
if os.system(" ".join((
|
||||
program,
|
||||
"-interaction=batchmode",
|
||||
"-halt-on-error",
|
||||
f"-output-directory=\"{os.path.dirname(svg_file)}\"",
|
||||
f"\"{root}.tex\"",
|
||||
">",
|
||||
os.devnull
|
||||
))):
|
||||
log.error(
|
||||
"LaTeX Error! Not a worry, it happens to the best of us."
|
||||
# Run dvisvgm and capture output directly
|
||||
process = subprocess.run(
|
||||
[
|
||||
"dvisvgm",
|
||||
dvi_path,
|
||||
"-n", # no fonts
|
||||
"-v", "0", # quiet
|
||||
"--stdout", # output to stdout instead of file
|
||||
],
|
||||
capture_output=True
|
||||
)
|
||||
error_str = ""
|
||||
with open(root + ".log", "r", encoding="utf-8") as log_file:
|
||||
error_match_obj = re.search(r"(?<=\n! ).*\n.*\n", log_file.read())
|
||||
if error_match_obj:
|
||||
error_str = error_match_obj.group()
|
||||
log.debug(
|
||||
f"The error could be:\n`{error_str}`",
|
||||
)
|
||||
raise LatexError(error_str)
|
||||
|
||||
# dvi to svg
|
||||
os.system(" ".join((
|
||||
"dvisvgm",
|
||||
f"\"{root}{dvi_ext}\"",
|
||||
"-n",
|
||||
"-v",
|
||||
"0",
|
||||
"-o",
|
||||
f"\"{svg_file}\"",
|
||||
">",
|
||||
os.devnull
|
||||
)))
|
||||
# Return SVG string
|
||||
result = process.stdout.decode('utf-8')
|
||||
|
||||
# Cleanup superfluous documents
|
||||
for ext in (".tex", dvi_ext, ".log", ".aux"):
|
||||
try:
|
||||
os.remove(root + ext)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if show_message_during_execution:
|
||||
print(" " * len(message), end="\r")
|
||||
|
||||
|
||||
# TODO, perhaps this should live elsewhere
|
||||
@contextmanager
|
||||
def display_during_execution(message: str):
|
||||
# Merge into a single line
|
||||
to_print = message.replace("\n", " ")
|
||||
max_characters = os.get_terminal_size().columns - 1
|
||||
if len(to_print) > max_characters:
|
||||
to_print = to_print[:max_characters - 3] + "..."
|
||||
try:
|
||||
print(to_print, end="\r")
|
||||
yield
|
||||
finally:
|
||||
print(" " * len(to_print), end="\r")
|
||||
return result
|
||||
|
||||
|
||||
class LatexError(Exception):
|
||||
|
|
|
@ -8,8 +8,8 @@ from moderngl_window.timers.clock import Timer
|
|||
from screeninfo import get_monitors
|
||||
from functools import wraps
|
||||
|
||||
from manimlib.config import get_global_config
|
||||
from manimlib.constants import FRAME_SHAPE
|
||||
from manimlib.utils.customization import get_customization
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
@ -86,9 +86,10 @@ class Window(PygletWindow):
|
|||
self.size = (w, h)
|
||||
|
||||
def find_initial_position(self, size: tuple[int, int]) -> tuple[int, int]:
|
||||
custom_position = get_customization()["window_position"]
|
||||
global_config = get_global_config()
|
||||
custom_position = global_config["window_position"]
|
||||
mon_index = global_config["window_monitor"]
|
||||
monitors = get_monitors()
|
||||
mon_index = get_customization()["window_monitor"]
|
||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||
window_width, window_height = size
|
||||
# Position might be specified with a string of the form
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
appdirs
|
||||
colour
|
||||
diskcache
|
||||
ipython>=8.18.0
|
||||
isosurfaces
|
||||
fontTools
|
||||
|
@ -20,6 +22,7 @@ screeninfo
|
|||
skia-pathops
|
||||
svgelements>=1.8.1
|
||||
sympy
|
||||
tempfile
|
||||
tqdm
|
||||
typing-extensions; python_version < "3.11"
|
||||
validators
|
||||
|
|
Loading…
Add table
Reference in a new issue