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.scene.scene import *
|
||||||
|
|
||||||
from manimlib.utils.bezier import *
|
from manimlib.utils.bezier import *
|
||||||
|
from manimlib.utils.cache import *
|
||||||
from manimlib.utils.color import *
|
from manimlib.utils.color import *
|
||||||
from manimlib.utils.dict_ops import *
|
from manimlib.utils.dict_ops import *
|
||||||
from manimlib.utils.customization import *
|
|
||||||
from manimlib.utils.debug import *
|
from manimlib.utils.debug import *
|
||||||
from manimlib.utils.directories import *
|
from manimlib.utils.directories import *
|
||||||
from manimlib.utils.file_ops import *
|
from manimlib.utils.file_ops import *
|
||||||
|
|
|
@ -10,6 +10,8 @@ import screeninfo
|
||||||
import sys
|
import sys
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
from manimlib.utils.dict_ops import merge_dicts_recursively
|
from manimlib.utils.dict_ops import merge_dicts_recursively
|
||||||
from manimlib.utils.init_config import init_customization
|
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
|
from typing import TYPE_CHECKING
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
Module = importlib.util.types.ModuleType
|
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():
|
def parse_cli():
|
||||||
|
@ -300,69 +305,30 @@ def get_scene_module(args: Namespace) -> Module:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_custom_config():
|
def load_yaml(file_path: str):
|
||||||
global __config_file__
|
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")
|
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):
|
# Set the subdirectories
|
||||||
with open(global_defaults_file, "r") as file:
|
base = config['directories']['base']
|
||||||
custom_config = yaml.safe_load(file)
|
for key, subdir in config['directories']['subdirs'].items():
|
||||||
|
config['directories'][key] = os.path.join(base, subdir)
|
||||||
|
|
||||||
if os.path.exists(__config_file__):
|
return config
|
||||||
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`"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_file_ext(args: Namespace) -> str:
|
def get_file_ext(args: Namespace) -> str:
|
||||||
|
@ -435,7 +401,8 @@ def get_window_config(args: Namespace, custom_config: dict, camera_config: dict)
|
||||||
try:
|
try:
|
||||||
monitors = screeninfo.get_monitors()
|
monitors = screeninfo.get_monitors()
|
||||||
except screeninfo.ScreenInfoError:
|
except screeninfo.ScreenInfoError:
|
||||||
pass
|
# Default fallback
|
||||||
|
monitors = [screeninfo.Monitor(width=1920, height=1080)]
|
||||||
mon_index = custom_config["window_monitor"]
|
mon_index = custom_config["window_monitor"]
|
||||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||||
aspect_ratio = camera_config["pixel_width"] / camera_config["pixel_height"]
|
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))
|
return dict(size=(window_width, window_height))
|
||||||
|
|
||||||
|
|
||||||
def get_camera_config(args: Namespace, custom_config: dict) -> dict:
|
def get_camera_config(args: Optional[Namespace] = None, custom_config: Optional[dict] = None) -> dict:
|
||||||
camera_config = {}
|
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"]
|
camera_resolutions = custom_config["camera_resolutions"]
|
||||||
if args.resolution:
|
if args.resolution:
|
||||||
resolution = args.resolution
|
resolution = args.resolution
|
||||||
|
@ -475,7 +447,7 @@ def get_camera_config(args: Namespace, custom_config: dict) -> dict:
|
||||||
"pixel_width": width,
|
"pixel_width": width,
|
||||||
"pixel_height": height,
|
"pixel_height": height,
|
||||||
"frame_config": {
|
"frame_config": {
|
||||||
"frame_shape": ((width / height) * get_frame_height(), get_frame_height()),
|
"frame_shape": ((width / height) * FRAME_HEIGHT, FRAME_HEIGHT),
|
||||||
},
|
},
|
||||||
"fps": fps,
|
"fps": fps,
|
||||||
})
|
})
|
||||||
|
@ -496,21 +468,19 @@ def get_camera_config(args: Namespace, custom_config: dict) -> dict:
|
||||||
return camera_config
|
return camera_config
|
||||||
|
|
||||||
|
|
||||||
def get_configuration(args: Namespace) -> dict:
|
def get_scene_config(args: Namespace) -> dict:
|
||||||
init_global_config(args.config_file)
|
"""
|
||||||
custom_config = get_custom_config()
|
Returns a dictionary to be used as key word arguments for Scene
|
||||||
camera_config = get_camera_config(args, custom_config)
|
"""
|
||||||
window_config = get_window_config(args, custom_config, camera_config)
|
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)
|
start, end = get_animations_numbers(args)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"module": get_scene_module(args),
|
"file_writer_config": get_file_writer_config(args, global_config),
|
||||||
"scene_names": args.scene_names,
|
|
||||||
"file_writer_config": get_file_writer_config(args, custom_config),
|
|
||||||
"camera_config": camera_config,
|
"camera_config": camera_config,
|
||||||
"window_config": window_config,
|
"window_config": window_config,
|
||||||
"quiet": args.quiet or args.write_all,
|
|
||||||
"write_all": args.write_all,
|
|
||||||
"skip_animations": args.skip_animations,
|
"skip_animations": args.skip_animations,
|
||||||
"start_at_animation_number": start,
|
"start_at_animation_number": start,
|
||||||
"end_at_animation_number": end,
|
"end_at_animation_number": end,
|
||||||
|
@ -518,26 +488,16 @@ def get_configuration(args: Namespace) -> dict:
|
||||||
"presenter_mode": args.presenter_mode,
|
"presenter_mode": args.presenter_mode,
|
||||||
"leave_progress_bars": args.leave_progress_bars,
|
"leave_progress_bars": args.leave_progress_bars,
|
||||||
"show_animation_progress": args.show_animation_progress,
|
"show_animation_progress": args.show_animation_progress,
|
||||||
"prerun": args.prerun,
|
"embed_exception_mode": global_config["embed_exception_mode"],
|
||||||
"embed_exception_mode": custom_config["embed_exception_mode"],
|
"embed_error_sound": global_config["embed_error_sound"],
|
||||||
"embed_error_sound": custom_config["embed_error_sound"],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_frame_height():
|
def get_run_config(args: Namespace):
|
||||||
return 8.0
|
return {
|
||||||
|
"module": get_scene_module(args),
|
||||||
|
"prerun": args.prerun,
|
||||||
def get_aspect_ratio():
|
"scene_names": args.scene_names,
|
||||||
cam_config = get_camera_config(parse_cli(), get_custom_config())
|
"quiet": args.quiet or args.write_all,
|
||||||
return cam_config['pixel_width'] / cam_config['pixel_height']
|
"write_all": args.write_all,
|
||||||
|
}
|
||||||
|
|
||||||
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']
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from manimlib.config import get_aspect_ratio
|
from manimlib.config import get_camera_config
|
||||||
from manimlib.config import get_default_pixel_width
|
from manimlib.config import FRAME_HEIGHT
|
||||||
from manimlib.config import get_default_pixel_height
|
|
||||||
from manimlib.config import get_frame_height
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -12,16 +10,20 @@ if TYPE_CHECKING:
|
||||||
from manimlib.typing import ManimColor, Vect3
|
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
|
# Sizes relevant to default camera frame
|
||||||
ASPECT_RATIO: float = get_aspect_ratio()
|
ASPECT_RATIO: float = CAMERA_CONFIG['pixel_width'] / CAMERA_CONFIG['pixel_height']
|
||||||
FRAME_HEIGHT: float = get_frame_height()
|
|
||||||
FRAME_WIDTH: float = FRAME_HEIGHT * ASPECT_RATIO
|
FRAME_WIDTH: float = FRAME_HEIGHT * ASPECT_RATIO
|
||||||
FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT)
|
FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT)
|
||||||
FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2
|
FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2
|
||||||
FRAME_X_RADIUS: float = FRAME_WIDTH / 2
|
FRAME_X_RADIUS: float = FRAME_WIDTH / 2
|
||||||
|
|
||||||
DEFAULT_PIXEL_HEIGHT: int = get_default_pixel_height()
|
DEFAULT_PIXEL_HEIGHT: int = CAMERA_CONFIG['pixel_height']
|
||||||
DEFAULT_PIXEL_WIDTH: int = get_default_pixel_width()
|
DEFAULT_PIXEL_WIDTH: int = CAMERA_CONFIG['pixel_width']
|
||||||
DEFAULT_FPS: int = 30
|
DEFAULT_FPS: int = 30
|
||||||
|
|
||||||
SMALL_BUFF: float = 0.1
|
SMALL_BUFF: float = 0.1
|
||||||
|
|
|
@ -3,19 +3,28 @@ directories:
|
||||||
# to match the directory structure of the path to the
|
# to match the directory structure of the path to the
|
||||||
# sourcecode generating that video
|
# sourcecode generating that video
|
||||||
mirror_module_path: False
|
mirror_module_path: False
|
||||||
# Where should manim output video and image files?
|
# Manim may write to and read from teh file system, e.g.
|
||||||
output: ""
|
# to render videos and to look for svg/png assets. This
|
||||||
# If you want to use images, manim will look to these folders to find them
|
# will specify where those assets live, with a base directory,
|
||||||
raster_images: ""
|
# and various subdirectory names within it
|
||||||
vector_images: ""
|
base: ""
|
||||||
# If you want to use sounds, manim will look here to find it.
|
subdirs:
|
||||||
sounds: ""
|
# Where should manim output video and image files?
|
||||||
# Manim often generates tex_files or other kinds of serialized data
|
output: "videos"
|
||||||
# to keep from having to generate the same thing too many times. By
|
# If you want to use images, manim will look to these folders to find them
|
||||||
# default, these will be stored at tempfile.gettempdir(), e.g. this might
|
raster_images: "raster_images"
|
||||||
# return whatever is at to the TMPDIR environment variable. If you want to
|
vector_images: "vector_images"
|
||||||
# specify them elsewhere,
|
# If you want to use sounds, manim will look here to find it.
|
||||||
temporary_storage: ""
|
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 *"
|
universal_import_line: "from manimlib import *"
|
||||||
style:
|
style:
|
||||||
tex_template: "default"
|
tex_template: "default"
|
||||||
|
|
|
@ -2,7 +2,7 @@ import copy
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from manimlib.config import get_custom_config
|
from manimlib.config import get_global_config
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
from manimlib.scene.interactive_scene import InteractiveScene
|
from manimlib.scene.interactive_scene import InteractiveScene
|
||||||
from manimlib.scene.scene import Scene
|
from manimlib.scene.scene import Scene
|
||||||
|
@ -10,7 +10,7 @@ from manimlib.scene.scene import Scene
|
||||||
|
|
||||||
class BlankScene(InteractiveScene):
|
class BlankScene(InteractiveScene):
|
||||||
def construct(self):
|
def construct(self):
|
||||||
exec(get_custom_config()["universal_import_line"])
|
exec(get_global_config()["universal_import_line"])
|
||||||
self.embed()
|
self.embed()
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,14 +53,6 @@ def prompt_user_for_choice(scene_classes):
|
||||||
sys.exit(1)
|
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):
|
def compute_total_frames(scene_class, scene_config):
|
||||||
"""
|
"""
|
||||||
When a scene is being written to file, a copy of the scene is run with
|
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"])
|
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"]
|
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)
|
fw_config["total_frames"] = compute_total_frames(scene_class, scene_config)
|
||||||
return scene_class(**scene_config)
|
return scene_class(**scene_config)
|
||||||
|
|
||||||
|
|
||||||
def get_scenes_to_render(all_scene_classes, scene_config, config):
|
def get_scenes_to_render(all_scene_classes, scene_config, run_config):
|
||||||
if config["write_all"]:
|
if run_config["write_all"]:
|
||||||
return [sc(**scene_config) for sc in all_scene_classes]
|
return [sc(**scene_config) for sc in all_scene_classes]
|
||||||
|
|
||||||
names_to_classes = {sc.__name__ : sc for sc in all_scene_classes}
|
names_to_classes = {sc.__name__: sc for sc in all_scene_classes}
|
||||||
scene_names = config["scene_names"]
|
scene_names = run_config["scene_names"]
|
||||||
|
|
||||||
for name in set.difference(set(scene_names), names_to_classes):
|
for name in set.difference(set(scene_names), names_to_classes):
|
||||||
log.error(f"No scene named {name} found")
|
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)
|
classes_to_run = prompt_user_for_choice(all_scene_classes)
|
||||||
|
|
||||||
return [
|
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
|
for scene_class in classes_to_run
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -123,13 +115,10 @@ def get_scene_classes_from_module(module):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def main(config):
|
def main(scene_config, run_config):
|
||||||
module = config["module"]
|
if run_config["module"] is None:
|
||||||
scene_config = get_scene_config(config)
|
|
||||||
if module is None:
|
|
||||||
# If no module was passed in, just play the blank scene
|
# If no module was passed in, just play the blank scene
|
||||||
return [BlankScene(**scene_config)]
|
return [BlankScene(**scene_config)]
|
||||||
|
|
||||||
all_scene_classes = get_scene_classes_from_module(module)
|
all_scene_classes = get_scene_classes_from_module(run_config["module"])
|
||||||
scenes = get_scenes_to_render(all_scene_classes, scene_config, config)
|
return get_scenes_to_render(all_scene_classes, scene_config, run_config)
|
||||||
return scenes
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from colour import Color
|
from colour import Color
|
||||||
|
|
||||||
|
from manimlib.config import get_global_config
|
||||||
from manimlib.constants import BLACK, RED, YELLOW, WHITE
|
from manimlib.constants import BLACK, RED, YELLOW, WHITE
|
||||||
from manimlib.constants import DL, DOWN, DR, LEFT, RIGHT, UL, UR
|
from manimlib.constants import DL, DOWN, DR, LEFT, RIGHT, UL, UR
|
||||||
from manimlib.constants import SMALL_BUFF
|
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.geometry import Rectangle
|
||||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||||
from manimlib.utils.customization import get_customization
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ class BackgroundRectangle(SurroundingRectangle):
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
if color is None:
|
if color is None:
|
||||||
color = get_customization()['style']['background_color']
|
color = get_global_config()['style']['background_color']
|
||||||
super().__init__(
|
super().__init__(
|
||||||
mobject,
|
mobject,
|
||||||
color=color,
|
color=color,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import re
|
||||||
from manimlib.constants import BLACK, WHITE
|
from manimlib.constants import BLACK, WHITE
|
||||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
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
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
@ -76,12 +76,8 @@ class SingleStringTex(SVGMobject):
|
||||||
self.additional_preamble
|
self.additional_preamble
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_file_path(self) -> str:
|
def get_svg_string_by_content(self, content: str) -> str:
|
||||||
content = self.get_tex_file_body(self.tex_string)
|
return latex_to_svg(content, self.template, self.additional_preamble)
|
||||||
file_path = tex_content_to_svg_file(
|
|
||||||
content, self.template, self.additional_preamble, self.tex_string
|
|
||||||
)
|
|
||||||
return file_path
|
|
||||||
|
|
||||||
def get_tex_file_body(self, tex_string: str) -> str:
|
def get_tex_file_body(self, tex_string: str) -> str:
|
||||||
new_tex = self.get_modified_expression(tex_string)
|
new_tex = self.get_modified_expression(tex_string)
|
||||||
|
|
|
@ -66,17 +66,18 @@ class StringMobject(SVGMobject, ABC):
|
||||||
self.use_labelled_svg = use_labelled_svg
|
self.use_labelled_svg = use_labelled_svg
|
||||||
|
|
||||||
self.parse()
|
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_stroke(stroke_color, stroke_width)
|
||||||
self.set_fill(fill_color, border_width=fill_border_width)
|
self.set_fill(fill_color, border_width=fill_border_width)
|
||||||
self.labels = [submob.label for submob in self.submobjects]
|
self.labels = [submob.label for submob in self.submobjects]
|
||||||
|
|
||||||
def get_file_path(self, is_labelled: bool = False) -> str:
|
def get_svg_string(self, is_labelled: bool = False) -> str:
|
||||||
is_labelled = is_labelled or self.use_labelled_svg
|
content = self.get_content(is_labelled or self.use_labelled_svg)
|
||||||
return self.get_file_path_by_content(self.get_content(is_labelled))
|
return self.get_svg_string_by_content(content)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_file_path_by_content(self, content: str) -> str:
|
def get_svg_string_by_content(self, content: str) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def assign_labels_by_color(self, mobjects: list[VMobject]) -> None:
|
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]:
|
def mobjects_from_svg_string(self, svg_string: str) -> list[VMobject]:
|
||||||
submobs = super().mobjects_from_file(file_path)
|
submobs = super().mobjects_from_svg_string(svg_string)
|
||||||
|
|
||||||
if self.use_labelled_svg:
|
if self.use_labelled_svg:
|
||||||
# This means submobjects are colored according to spans
|
# This means submobjects are colored according to spans
|
||||||
|
|
|
@ -5,6 +5,7 @@ from xml.etree import ElementTree as ET
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import svgelements as se
|
import svgelements as se
|
||||||
import io
|
import io
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from manimlib.constants import RIGHT
|
from manimlib.constants import RIGHT
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
|
@ -39,6 +40,7 @@ class SVGMobject(VMobject):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
file_name: str = "",
|
file_name: str = "",
|
||||||
|
svg_string: str = "",
|
||||||
should_center: bool = True,
|
should_center: bool = True,
|
||||||
height: float | None = None,
|
height: float | None = None,
|
||||||
width: float | None = None,
|
width: float | None = None,
|
||||||
|
@ -63,11 +65,19 @@ class SVGMobject(VMobject):
|
||||||
path_string_config: dict = dict(),
|
path_string_config: dict = dict(),
|
||||||
**kwargs
|
**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.svg_default = dict(svg_default)
|
||||||
self.path_string_config = dict(path_string_config)
|
self.path_string_config = dict(path_string_config)
|
||||||
|
|
||||||
super().__init__(**kwargs )
|
super().__init__(**kwargs)
|
||||||
self.init_svg_mobject()
|
self.init_svg_mobject()
|
||||||
self.ensure_positive_orientation()
|
self.ensure_positive_orientation()
|
||||||
|
|
||||||
|
@ -97,7 +107,7 @@ class SVGMobject(VMobject):
|
||||||
if hash_val in SVG_HASH_TO_MOB_MAP:
|
if hash_val in SVG_HASH_TO_MOB_MAP:
|
||||||
submobs = [sm.copy() for sm in SVG_HASH_TO_MOB_MAP[hash_val]]
|
submobs = [sm.copy() for sm in SVG_HASH_TO_MOB_MAP[hash_val]]
|
||||||
else:
|
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]
|
SVG_HASH_TO_MOB_MAP[hash_val] = [sm.copy() for sm in submobs]
|
||||||
|
|
||||||
self.add(*submobs)
|
self.add(*submobs)
|
||||||
|
@ -111,11 +121,11 @@ class SVGMobject(VMobject):
|
||||||
self.__class__.__name__,
|
self.__class__.__name__,
|
||||||
self.svg_default,
|
self.svg_default,
|
||||||
self.path_string_config,
|
self.path_string_config,
|
||||||
self.file_name
|
self.svg_string
|
||||||
)
|
)
|
||||||
|
|
||||||
def mobjects_from_file(self, file_path: str) -> list[VMobject]:
|
def mobjects_from_svg_string(self, svg_string: str) -> list[VMobject]:
|
||||||
element_tree = ET.parse(file_path)
|
element_tree = ET.ElementTree(ET.fromstring(svg_string))
|
||||||
new_tree = self.modify_xml_tree(element_tree)
|
new_tree = self.modify_xml_tree(element_tree)
|
||||||
|
|
||||||
# New svg based on tree contents
|
# New svg based on tree contents
|
||||||
|
@ -127,10 +137,8 @@ class SVGMobject(VMobject):
|
||||||
|
|
||||||
return self.mobjects_from_svg(svg)
|
return self.mobjects_from_svg(svg)
|
||||||
|
|
||||||
def get_file_path(self) -> str:
|
def file_name_to_svg_string(self, file_name: str) -> str:
|
||||||
if self.file_name is None:
|
return Path(get_full_vector_image_path(file_name)).read_text()
|
||||||
raise Exception("Must specify file for SVGMobject")
|
|
||||||
return get_full_vector_image_path(self.file_name)
|
|
||||||
|
|
||||||
def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree:
|
def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree:
|
||||||
config_style_attrs = self.generate_config_style_dict()
|
config_style_attrs = self.generate_config_style_dict()
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from manimlib.mobject.svg.string_mobject import StringMobject
|
from manimlib.mobject.svg.string_mobject import StringMobject
|
||||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||||
from manimlib.utils.color import color_to_hex
|
from manimlib.utils.color import color_to_hex
|
||||||
from manimlib.utils.color import hex_to_int
|
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.tex import num_tex_symbols
|
||||||
|
from manimlib.utils.simple_functions import hash_string
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
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.set_color_by_tex_to_color_map(self.tex_to_color_map)
|
||||||
self.scale(SCALE_FACTOR_PER_FONT_POINT * font_size)
|
self.scale(SCALE_FACTOR_PER_FONT_POINT * font_size)
|
||||||
|
|
||||||
@property
|
def get_svg_string_by_content(self, content: str) -> str:
|
||||||
def hash_seed(self) -> tuple:
|
return latex_to_svg(content, self.template, self.additional_preamble, short_tex=self.tex_string)
|
||||||
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 _handle_scale_side_effects(self, scale_factor: float) -> Self:
|
def _handle_scale_side_effects(self, scale_factor: float) -> Self:
|
||||||
self.font_size *= scale_factor
|
self.font_size *= scale_factor
|
||||||
|
|
|
@ -4,21 +4,23 @@ from contextlib import contextmanager
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
import tempfile
|
||||||
|
import hashlib
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
import manimpango
|
import manimpango
|
||||||
import pygments
|
import pygments
|
||||||
import pygments.formatters
|
import pygments.formatters
|
||||||
import pygments.lexers
|
import pygments.lexers
|
||||||
|
|
||||||
|
from manimlib.config import get_global_config
|
||||||
from manimlib.constants import DEFAULT_PIXEL_WIDTH, FRAME_WIDTH
|
from manimlib.constants import DEFAULT_PIXEL_WIDTH, FRAME_WIDTH
|
||||||
from manimlib.constants import NORMAL
|
from manimlib.constants import NORMAL
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
from manimlib.mobject.svg.string_mobject import StringMobject
|
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 color_to_hex
|
||||||
from manimlib.utils.color import int_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 manimlib.utils.simple_functions import hash_string
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
@ -49,6 +51,58 @@ class _Alignment:
|
||||||
self.value = _Alignment.VAL_DICT[s.upper()]
|
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):
|
class MarkupText(StringMobject):
|
||||||
# See https://docs.gtk.org/Pango/pango_markup.html
|
# See https://docs.gtk.org/Pango/pango_markup.html
|
||||||
MARKUP_TAGS = {
|
MARKUP_TAGS = {
|
||||||
|
@ -102,13 +156,14 @@ class MarkupText(StringMobject):
|
||||||
isolate: Selector = re.compile(r"\w+", re.U),
|
isolate: Selector = re.compile(r"\w+", re.U),
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
|
default_style = get_global_config()["style"]
|
||||||
self.text = text
|
self.text = text
|
||||||
self.font_size = font_size
|
self.font_size = font_size
|
||||||
self.justify = justify
|
self.justify = justify
|
||||||
self.indent = indent
|
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.line_width = line_width
|
||||||
self.font = font or get_customization()["style"]["font"]
|
self.font = font or default_style["font"]
|
||||||
self.slant = slant
|
self.slant = slant
|
||||||
self.weight = weight
|
self.weight = weight
|
||||||
|
|
||||||
|
@ -141,88 +196,14 @@ class MarkupText(StringMobject):
|
||||||
if height is None:
|
if height is None:
|
||||||
self.scale(TEXT_MOB_SCALE_FACTOR)
|
self.scale(TEXT_MOB_SCALE_FACTOR)
|
||||||
|
|
||||||
@property
|
def get_svg_string_by_content(self, content: str) -> str:
|
||||||
def hash_seed(self) -> tuple:
|
self.content = content
|
||||||
return (
|
return markup_to_svg(
|
||||||
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((
|
|
||||||
content,
|
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,
|
justify=self.justify,
|
||||||
indent=self.indent,
|
indent=self.indent,
|
||||||
line_spacing=None, # Already handled
|
alignment=self.alignment,
|
||||||
alignment=alignment,
|
line_width=self.line_width
|
||||||
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}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Toolkits
|
# Toolkits
|
||||||
|
@ -511,20 +492,10 @@ def register_font(font_file: str | Path):
|
||||||
method with previous releases will raise an :class:`AttributeError` on macOS.
|
method with previous releases will raise an :class:`AttributeError` on macOS.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
input_folder = Path(get_downloads_dir()).parent.resolve()
|
file_path = Path(font_file).resolve()
|
||||||
possible_paths = [
|
if not file_path.exists():
|
||||||
Path(font_file),
|
error = f"Can't find {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}"
|
|
||||||
raise FileNotFoundError(error)
|
raise FileNotFoundError(error)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
assert manimpango.register_font(str(file_path))
|
assert manimpango.register_font(str(file_path))
|
||||||
yield
|
yield
|
||||||
|
|
|
@ -37,8 +37,6 @@ class ReloadManager:
|
||||||
except KillEmbedded:
|
except KillEmbedded:
|
||||||
# Requested via the `exit_raise` IPython runline magic
|
# Requested via the `exit_raise` IPython runline magic
|
||||||
# by means of our scene.reload() command
|
# by means of our scene.reload() command
|
||||||
print("Reloading...")
|
|
||||||
|
|
||||||
for scene in self.scenes:
|
for scene in self.scenes:
|
||||||
scene.tear_down()
|
scene.tear_down()
|
||||||
|
|
||||||
|
@ -61,12 +59,14 @@ class ReloadManager:
|
||||||
self.args.embed = str(overwrite_start_at_line)
|
self.args.embed = str(overwrite_start_at_line)
|
||||||
|
|
||||||
# Args to Config
|
# Args to Config
|
||||||
config = manimlib.config.get_configuration(self.args)
|
scene_config = manimlib.config.get_scene_config(self.args)
|
||||||
if self.window:
|
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
|
# Scenes
|
||||||
self.scenes = manimlib.extract_scene.main(config)
|
self.scenes = manimlib.extract_scene.main(scene_config, run_config)
|
||||||
if len(self.scenes) == 0:
|
if len(self.scenes) == 0:
|
||||||
print("No scenes found to run")
|
print("No scenes found to run")
|
||||||
return
|
return
|
||||||
|
@ -78,7 +78,13 @@ class ReloadManager:
|
||||||
break
|
break
|
||||||
|
|
||||||
for scene in self.scenes:
|
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()
|
scene.run()
|
||||||
|
|
||||||
|
|
||||||
reload_manager = ReloadManager()
|
reload_manager = ReloadManager()
|
||||||
|
|
|
@ -232,7 +232,11 @@ class Scene(object):
|
||||||
# the local namespace of the caller
|
# the local namespace of the caller
|
||||||
caller_frame = inspect.currentframe().f_back
|
caller_frame = inspect.currentframe().f_back
|
||||||
module = get_module(caller_frame.f_globals["__file__"])
|
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
|
self.shell = shell
|
||||||
|
|
||||||
# Add a few custom shortcuts to that local namespace
|
# Add a few custom shortcuts to that local namespace
|
||||||
|
@ -288,9 +292,6 @@ class Scene(object):
|
||||||
|
|
||||||
shell.set_custom_exc((Exception,), custom_exc)
|
shell.set_custom_exc((Exception,), custom_exc)
|
||||||
|
|
||||||
# Set desired exception mode
|
|
||||||
shell.magic(f"xmode {self.embed_exception_mode}")
|
|
||||||
|
|
||||||
# Launch shell
|
# Launch shell
|
||||||
shell()
|
shell()
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import numpy as np
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
from manimlib.config import parse_cli
|
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_code_from_file
|
||||||
from manimlib.utils.shaders import get_shader_program
|
from manimlib.utils.shaders import get_shader_program
|
||||||
from manimlib.utils.shaders import image_path_to_texture
|
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,
|
which can display that texture as a simple quad onto a screen,
|
||||||
along with the rgb value which is meant to be discarded.
|
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'])
|
size = (cam_config['pixel_width'], cam_config['pixel_height'])
|
||||||
double_size = (2 * size[0], 2 * size[1])
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
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
|
from manimlib.utils.file_ops import guarantee_existence
|
||||||
|
|
||||||
|
|
||||||
def get_directories() -> dict[str, str]:
|
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:
|
def get_temp_dir() -> str:
|
||||||
return get_directories()["temporary_storage"]
|
return get_directories()["temporary_storage"] or tempfile.gettempdir()
|
||||||
|
|
||||||
|
|
||||||
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"))
|
|
||||||
|
|
||||||
|
|
||||||
def get_downloads_dir() -> str:
|
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:
|
def get_output_dir() -> str:
|
||||||
|
@ -47,4 +43,4 @@ def get_sound_dir() -> str:
|
||||||
|
|
||||||
|
|
||||||
def get_shader_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 numpy as np
|
||||||
import validators
|
import validators
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
import manimlib.utils.directories
|
||||||
|
from manimlib.utils.simple_functions import hash_string
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
@ -35,11 +39,9 @@ def find_file(
|
||||||
# Check if this is a file online first, and if so, download
|
# Check if this is a file online first, and if so, download
|
||||||
# it to a temporary directory
|
# it to a temporary directory
|
||||||
if validators.url(file_name):
|
if validators.url(file_name):
|
||||||
import urllib.request
|
|
||||||
from manimlib.utils.directories import get_downloads_dir
|
|
||||||
suffix = Path(file_name).suffix
|
suffix = Path(file_name).suffix
|
||||||
file_hash = hashlib.sha256(file_name.encode('utf-8')).hexdigest()[:32]
|
file_hash = hash_string(file_name)
|
||||||
folder = get_downloads_dir()
|
folder = manimlib.utils.directories.get_downloads_dir()
|
||||||
|
|
||||||
path = Path(folder, file_hash).with_suffix(suffix)
|
path = Path(folder, file_hash).with_suffix(suffix)
|
||||||
urllib.request.urlretrieve(file_name, path)
|
urllib.request.urlretrieve(file_name, path)
|
||||||
|
|
|
@ -36,11 +36,15 @@ def init_customization() -> None:
|
||||||
configuration = {
|
configuration = {
|
||||||
"directories": {
|
"directories": {
|
||||||
"mirror_module_path": False,
|
"mirror_module_path": False,
|
||||||
"output": "",
|
"base": "",
|
||||||
"raster_images": "",
|
"subdirs": {
|
||||||
"vector_images": "",
|
"output": "videos",
|
||||||
"sounds": "",
|
"raster_images": "raster_images",
|
||||||
"temporary_storage": "",
|
"vector_images": "vector_images",
|
||||||
|
"sounds": "sounds",
|
||||||
|
"data": "data",
|
||||||
|
"downloads": "downloads",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"universal_import_line": "from manimlib import *",
|
"universal_import_line": "from manimlib import *",
|
||||||
"style": {
|
"style": {
|
||||||
|
@ -74,33 +78,39 @@ def init_customization() -> None:
|
||||||
|
|
||||||
console.print("[bold]Directories:[/bold]")
|
console.print("[bold]Directories:[/bold]")
|
||||||
dir_config = configuration["directories"]
|
dir_config = configuration["directories"]
|
||||||
dir_config["output"] = Prompt.ask(
|
dir_config["base"] = Prompt.ask(
|
||||||
" Where should manim [bold]output[/bold] video and image files place [prompt.default](optional, default is none)",
|
" What base directory should manim use for reading/writing video and images? [prompt.default](optional, default is none)",
|
||||||
default="",
|
default="",
|
||||||
show_default=False
|
show_default=False
|
||||||
)
|
)
|
||||||
dir_config["raster_images"] = Prompt.ask(
|
dir_config["subdirs"]["output"] = Prompt.ask(
|
||||||
" Which folder should manim find [bold]raster images[/bold] (.jpg .png .gif) in " + \
|
" Within that base directory, which subdirectory should manim [bold]output[/bold] video and image files to?" + \
|
||||||
"[prompt.default](optional, default is none)",
|
" [prompt.default](optional, default is \"videos\")",
|
||||||
default="",
|
default="videos",
|
||||||
show_default=False
|
show_default=False
|
||||||
)
|
)
|
||||||
dir_config["vector_images"] = Prompt.ask(
|
dir_config["subdirs"]["raster_images"] = Prompt.ask(
|
||||||
" Which folder should manim find [bold]vector images[/bold] (.svg .xdv) in " + \
|
" Within that base directory, which subdirectory should manim look for raster images (.png, .jpg)" + \
|
||||||
"[prompt.default](optional, default is none)",
|
" [prompt.default](optional, default is \"raster_images\")",
|
||||||
default="",
|
default="raster_images",
|
||||||
show_default=False
|
show_default=False
|
||||||
)
|
)
|
||||||
dir_config["sounds"] = Prompt.ask(
|
dir_config["subdirs"]["vector_images"] = Prompt.ask(
|
||||||
" Which folder should manim find [bold]sound files[/bold] (.mp3 .wav) in " + \
|
" Within that base directory, which subdirectory should manim look for raster images (.svg, .xdv)" + \
|
||||||
"[prompt.default](optional, default is none)",
|
" [prompt.default](optional, default is \"vector_images\")",
|
||||||
default="",
|
default="vector_images",
|
||||||
show_default=False
|
show_default=False
|
||||||
)
|
)
|
||||||
dir_config["temporary_storage"] = Prompt.ask(
|
dir_config["subdirs"]["sounds"] = Prompt.ask(
|
||||||
" Which folder should manim storage [bold]temporary files[/bold] " + \
|
" Within that base directory, which subdirectory should manim look for sound files (.mp3, .wav)" + \
|
||||||
"[prompt.default](recommended, use system temporary folder by default)",
|
" [prompt.default](optional, default is \"sounds\")",
|
||||||
default="",
|
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
|
show_default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,6 @@ def binary_search(function: Callable[[float], float],
|
||||||
return mh
|
return mh
|
||||||
|
|
||||||
|
|
||||||
def hash_string(string: str) -> str:
|
def hash_string(string: str, n_bytes=16) -> str:
|
||||||
# Truncating at 16 bytes for cleanliness
|
|
||||||
hasher = hashlib.sha256(string.encode())
|
hasher = hashlib.sha256(string.encode())
|
||||||
return hasher.hexdigest()[:16]
|
return hasher.hexdigest()[:n_bytes]
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from contextlib import contextmanager
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import yaml
|
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.config import get_manim_dir
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
from manimlib.utils.directories import get_tex_dir
|
|
||||||
from manimlib.utils.simple_functions import hash_string
|
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]:
|
def get_tex_template_config(template_name: str) -> dict[str, str]:
|
||||||
name = template_name.replace(" ", "_").lower()
|
name = template_name.replace(" ", "_").lower()
|
||||||
with open(os.path.join(
|
template_path = os.path.join(get_manim_dir(), "manimlib", "tex_templates.yml")
|
||||||
get_manim_dir(), "manimlib", "tex_templates.yml"
|
with open(template_path, encoding="utf-8") as tex_templates_file:
|
||||||
), encoding="utf-8") as tex_templates_file:
|
|
||||||
templates_dict = yaml.safe_load(tex_templates_file)
|
templates_dict = yaml.safe_load(tex_templates_file)
|
||||||
if name not in templates_dict:
|
if name not in templates_dict:
|
||||||
log.warning(
|
log.warning(
|
||||||
|
@ -41,7 +44,7 @@ def get_tex_config() -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
# Only load once, then save thereafter
|
# Only load once, then save thereafter
|
||||||
if not SAVED_TEX_CONFIG:
|
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)
|
template_config = get_tex_template_config(template_name)
|
||||||
SAVED_TEX_CONFIG.update({
|
SAVED_TEX_CONFIG.update({
|
||||||
"template": template_name,
|
"template": template_name,
|
||||||
|
@ -51,22 +54,8 @@ def get_tex_config() -> dict[str, str]:
|
||||||
return SAVED_TEX_CONFIG
|
return SAVED_TEX_CONFIG
|
||||||
|
|
||||||
|
|
||||||
def tex_content_to_svg_file(
|
def get_full_tex(content: str, preamble: str = ""):
|
||||||
content: str, template: str, additional_preamble: str,
|
return "\n\n".join((
|
||||||
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((
|
|
||||||
"\\documentclass[preview]{standalone}",
|
"\\documentclass[preview]{standalone}",
|
||||||
preamble,
|
preamble,
|
||||||
"\\begin{document}",
|
"\\begin{document}",
|
||||||
|
@ -74,17 +63,43 @@ def tex_content_to_svg_file(
|
||||||
"\\end{document}"
|
"\\end{document}"
|
||||||
)) + "\n"
|
)) + "\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":
|
if compiler == "latex":
|
||||||
program = "latex"
|
program = "latex"
|
||||||
dvi_ext = ".dvi"
|
dvi_ext = ".dvi"
|
||||||
|
@ -92,72 +107,65 @@ def create_tex_svg(full_tex: str, svg_file: str, compiler: str) -> None:
|
||||||
program = "xelatex -no-pdf"
|
program = "xelatex -no-pdf"
|
||||||
dvi_ext = ".xdv"
|
dvi_ext = ".xdv"
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(f"Compiler '{compiler}' is not implemented")
|
||||||
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
|
if process.returncode != 0:
|
||||||
root, _ = os.path.splitext(svg_file)
|
# Handle error
|
||||||
with open(root + ".tex", "w", encoding="utf-8") as tex_file:
|
error_str = ""
|
||||||
tex_file.write(full_tex)
|
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
|
# Run dvisvgm and capture output directly
|
||||||
if os.system(" ".join((
|
process = subprocess.run(
|
||||||
program,
|
[
|
||||||
"-interaction=batchmode",
|
"dvisvgm",
|
||||||
"-halt-on-error",
|
dvi_path,
|
||||||
f"-output-directory=\"{os.path.dirname(svg_file)}\"",
|
"-n", # no fonts
|
||||||
f"\"{root}.tex\"",
|
"-v", "0", # quiet
|
||||||
">",
|
"--stdout", # output to stdout instead of file
|
||||||
os.devnull
|
],
|
||||||
))):
|
capture_output=True
|
||||||
log.error(
|
|
||||||
"LaTeX Error! Not a worry, it happens to the best of us."
|
|
||||||
)
|
)
|
||||||
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
|
# Return SVG string
|
||||||
os.system(" ".join((
|
result = process.stdout.decode('utf-8')
|
||||||
"dvisvgm",
|
|
||||||
f"\"{root}{dvi_ext}\"",
|
|
||||||
"-n",
|
|
||||||
"-v",
|
|
||||||
"0",
|
|
||||||
"-o",
|
|
||||||
f"\"{svg_file}\"",
|
|
||||||
">",
|
|
||||||
os.devnull
|
|
||||||
)))
|
|
||||||
|
|
||||||
# Cleanup superfluous documents
|
if show_message_during_execution:
|
||||||
for ext in (".tex", dvi_ext, ".log", ".aux"):
|
print(" " * len(message), end="\r")
|
||||||
try:
|
|
||||||
os.remove(root + ext)
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
return result
|
||||||
# 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")
|
|
||||||
|
|
||||||
|
|
||||||
class LatexError(Exception):
|
class LatexError(Exception):
|
||||||
|
|
|
@ -8,8 +8,8 @@ from moderngl_window.timers.clock import Timer
|
||||||
from screeninfo import get_monitors
|
from screeninfo import get_monitors
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
|
from manimlib.config import get_global_config
|
||||||
from manimlib.constants import FRAME_SHAPE
|
from manimlib.constants import FRAME_SHAPE
|
||||||
from manimlib.utils.customization import get_customization
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
@ -86,9 +86,10 @@ class Window(PygletWindow):
|
||||||
self.size = (w, h)
|
self.size = (w, h)
|
||||||
|
|
||||||
def find_initial_position(self, size: tuple[int, int]) -> tuple[int, int]:
|
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()
|
monitors = get_monitors()
|
||||||
mon_index = get_customization()["window_monitor"]
|
|
||||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||||
window_width, window_height = size
|
window_width, window_height = size
|
||||||
# Position might be specified with a string of the form
|
# Position might be specified with a string of the form
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
appdirs
|
||||||
colour
|
colour
|
||||||
|
diskcache
|
||||||
ipython>=8.18.0
|
ipython>=8.18.0
|
||||||
isosurfaces
|
isosurfaces
|
||||||
fontTools
|
fontTools
|
||||||
|
@ -20,6 +22,7 @@ screeninfo
|
||||||
skia-pathops
|
skia-pathops
|
||||||
svgelements>=1.8.1
|
svgelements>=1.8.1
|
||||||
sympy
|
sympy
|
||||||
|
tempfile
|
||||||
tqdm
|
tqdm
|
||||||
typing-extensions; python_version < "3.11"
|
typing-extensions; python_version < "3.11"
|
||||||
validators
|
validators
|
||||||
|
|
Loading…
Add table
Reference in a new issue