mirror of
https://github.com/3b1b/manim.git
synced 2025-08-01 08:54:38 +08:00
Merge pull request #1818 from YishiMichael/refactor
Add `template` and `additional_preamble` parameters to `Tex`
This commit is contained in:
@ -12,6 +12,7 @@ from manimlib.animation.fading import FadeOut
|
||||
from manimlib.animation.fading import FadeIn
|
||||
from manimlib.animation.movement import Homotopy
|
||||
from manimlib.animation.transform import Transform
|
||||
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
|
||||
from manimlib.constants import ORIGIN, RIGHT, UP
|
||||
from manimlib.constants import SMALL_BUFF
|
||||
from manimlib.constants import TAU
|
||||
|
@ -167,83 +167,95 @@ class TransformMatchingStrings(AnimationGroup):
|
||||
digest_config(self, kwargs)
|
||||
assert isinstance(source, StringMobject)
|
||||
assert isinstance(target, StringMobject)
|
||||
anims = []
|
||||
source_indices = list(range(len(source.labels)))
|
||||
target_indices = list(range(len(target.labels)))
|
||||
|
||||
def get_filtered_indices_lists(indices_lists, rest_indices):
|
||||
def get_matched_indices_lists(*part_items_list):
|
||||
part_items_list_len = len(part_items_list)
|
||||
indexed_part_items = sorted(it.chain(*[
|
||||
[
|
||||
(substr, items_index, indices_list)
|
||||
for substr, indices_list in part_items
|
||||
]
|
||||
for items_index, part_items in enumerate(part_items_list)
|
||||
]))
|
||||
grouped_part_items = [
|
||||
(substr, [
|
||||
[indices_lists for _, _, indices_lists in grouper_2]
|
||||
for _, grouper_2 in it.groupby(
|
||||
grouper_1, key=lambda t: t[1]
|
||||
)
|
||||
])
|
||||
for substr, grouper_1 in it.groupby(
|
||||
indexed_part_items, key=lambda t: t[0]
|
||||
)
|
||||
]
|
||||
return [
|
||||
tuple(indices_lists_list)
|
||||
for _, indices_lists_list in sorted(filter(
|
||||
lambda t: t[0] and len(t[1]) == part_items_list_len,
|
||||
grouped_part_items
|
||||
), key=lambda t: len(t[0]), reverse=True)
|
||||
]
|
||||
|
||||
def get_filtered_indices_lists(indices_lists, used_indices):
|
||||
result = []
|
||||
used = []
|
||||
for indices_list in indices_lists:
|
||||
if not indices_list:
|
||||
continue
|
||||
if not all(index in rest_indices for index in indices_list):
|
||||
if not all(
|
||||
index not in used_indices and index not in used
|
||||
for index in indices_list
|
||||
):
|
||||
continue
|
||||
result.append(indices_list)
|
||||
for index in indices_list:
|
||||
rest_indices.remove(index)
|
||||
return result
|
||||
used.extend(indices_list)
|
||||
return result, used
|
||||
|
||||
def add_anims(anim_class, indices_lists_pairs):
|
||||
for source_indices_lists, target_indices_lists in indices_lists_pairs:
|
||||
source_indices_lists = get_filtered_indices_lists(
|
||||
source_indices_lists, source_indices
|
||||
)
|
||||
target_indices_lists = get_filtered_indices_lists(
|
||||
target_indices_lists, target_indices
|
||||
)
|
||||
if not source_indices_lists or not target_indices_lists:
|
||||
source_indices.extend(it.chain(*source_indices_lists))
|
||||
target_indices.extend(it.chain(*target_indices_lists))
|
||||
continue
|
||||
anims.append(anim_class(
|
||||
source.build_parts_from_indices_lists(source_indices_lists),
|
||||
target.build_parts_from_indices_lists(target_indices_lists),
|
||||
**kwargs
|
||||
))
|
||||
|
||||
def get_substr_to_indices_lists_map(part_items):
|
||||
result = {}
|
||||
for substr, indices_list in part_items:
|
||||
if substr not in result:
|
||||
result[substr] = []
|
||||
result[substr].append(indices_list)
|
||||
return result
|
||||
|
||||
def add_anims_from(anim_class, func):
|
||||
source_substr_map = get_substr_to_indices_lists_map(func(source))
|
||||
target_substr_map = get_substr_to_indices_lists_map(func(target))
|
||||
common_substrings = sorted([
|
||||
s for s in source_substr_map if s and s in target_substr_map
|
||||
], key=len, reverse=True)
|
||||
add_anims(
|
||||
anim_class,
|
||||
[
|
||||
(source_substr_map[substr], target_substr_map[substr])
|
||||
for substr in common_substrings
|
||||
]
|
||||
)
|
||||
|
||||
add_anims(
|
||||
ReplacementTransform,
|
||||
[
|
||||
anim_class_items = [
|
||||
(ReplacementTransform, [
|
||||
(
|
||||
source.get_submob_indices_lists_by_selector(k),
|
||||
target.get_submob_indices_lists_by_selector(v)
|
||||
)
|
||||
for k, v in self.key_map.items()
|
||||
]
|
||||
)
|
||||
add_anims_from(
|
||||
FadeTransformPieces,
|
||||
StringMobject.get_specified_part_items
|
||||
)
|
||||
add_anims_from(
|
||||
FadeTransformPieces,
|
||||
StringMobject.get_group_part_items
|
||||
)
|
||||
]),
|
||||
(FadeTransformPieces, get_matched_indices_lists(
|
||||
source.get_specified_part_items(),
|
||||
target.get_specified_part_items()
|
||||
)),
|
||||
(FadeTransformPieces, get_matched_indices_lists(
|
||||
source.get_group_part_items(),
|
||||
target.get_group_part_items()
|
||||
))
|
||||
]
|
||||
|
||||
rest_source = VGroup(*[source[index] for index in source_indices])
|
||||
rest_target = VGroup(*[target[index] for index in target_indices])
|
||||
anims = []
|
||||
source_used_indices = []
|
||||
target_used_indices = []
|
||||
for anim_class, pairs in anim_class_items:
|
||||
for source_indices_lists, target_indices_lists in pairs:
|
||||
source_filtered, source_used = get_filtered_indices_lists(
|
||||
source_indices_lists, source_used_indices
|
||||
)
|
||||
target_filtered, target_used = get_filtered_indices_lists(
|
||||
target_indices_lists, target_used_indices
|
||||
)
|
||||
if not source_filtered or not target_filtered:
|
||||
continue
|
||||
anims.append(anim_class(
|
||||
source.build_parts_from_indices_lists(source_filtered),
|
||||
target.build_parts_from_indices_lists(target_filtered),
|
||||
**kwargs
|
||||
))
|
||||
source_used_indices.extend(source_used)
|
||||
target_used_indices.extend(target_used)
|
||||
|
||||
rest_source = VGroup(*[
|
||||
submob for index, submob in enumerate(source.submobjects)
|
||||
if index not in source_used_indices
|
||||
])
|
||||
rest_target = VGroup(*[
|
||||
submob for index, submob in enumerate(target.submobjects)
|
||||
if index not in target_used_indices
|
||||
])
|
||||
if self.transform_mismatches:
|
||||
anims.append(
|
||||
ReplacementTransform(rest_source, rest_target, **kwargs)
|
||||
|
@ -16,17 +16,9 @@ directories:
|
||||
# return whatever is at to the TMPDIR environment variable. If you want to
|
||||
# specify them elsewhere,
|
||||
temporary_storage: ""
|
||||
tex:
|
||||
executable: "latex"
|
||||
template_file: "tex_template.tex"
|
||||
intermediate_filetype: "dvi"
|
||||
text_to_replace: "[tex_expression]"
|
||||
# For ctex, use the following configuration
|
||||
# executable: "xelatex -no-pdf"
|
||||
# template_file: "ctex_template.tex"
|
||||
# intermediate_filetype: "xdv"
|
||||
universal_import_line: "from manimlib import *"
|
||||
style:
|
||||
tex_template: "default"
|
||||
font: "Consolas"
|
||||
text_alignment: "LEFT"
|
||||
background_color: "#333333"
|
||||
@ -49,4 +41,4 @@ camera_resolutions:
|
||||
high: "1920x1080"
|
||||
4k: "3840x2160"
|
||||
default_resolution: "high"
|
||||
fps: 30
|
||||
fps: 30
|
||||
|
@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from manimlib.mobject.svg.string_mobject import StringMobject
|
||||
from manimlib.utils.tex_file_writing import display_during_execution
|
||||
from manimlib.utils.tex_file_writing import get_tex_config
|
||||
from manimlib.utils.tex_file_writing import tex_to_svg_file
|
||||
from manimlib.utils.tex_file_writing import tex_content_to_svg_file
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@ -37,6 +38,8 @@ class MTex(StringMobject):
|
||||
"alignment": "\\centering",
|
||||
"tex_environment": "align*",
|
||||
"tex_to_color_map": {},
|
||||
"template": "",
|
||||
"additional_preamble": "",
|
||||
}
|
||||
|
||||
def __init__(self, tex_string: str, **kwargs):
|
||||
@ -57,77 +60,112 @@ class MTex(StringMobject):
|
||||
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.tex_to_color_map,
|
||||
self.template,
|
||||
self.additional_preamble
|
||||
)
|
||||
|
||||
def get_file_path_by_content(self, content: str) -> str:
|
||||
tex_config = get_tex_config()
|
||||
full_tex = tex_config["tex_body"].replace(
|
||||
tex_config["text_to_replace"],
|
||||
content
|
||||
)
|
||||
with display_during_execution(f"Writing \"{self.string}\""):
|
||||
file_path = tex_to_svg_file(full_tex)
|
||||
with display_during_execution(f"Writing \"{self.tex_string}\""):
|
||||
file_path = tex_content_to_svg_file(
|
||||
content, self.template, self.additional_preamble
|
||||
)
|
||||
return file_path
|
||||
|
||||
# Parsing
|
||||
|
||||
def get_cmd_spans(self) -> list[Span]:
|
||||
return self.find_spans(r"\\(?:[a-zA-Z]+|\s|\S)|[_^{}]")
|
||||
|
||||
def get_substr_flag(self, substr: str) -> int:
|
||||
return {"{": 1, "}": -1}.get(substr, 0)
|
||||
|
||||
def get_repl_substr_for_content(self, substr: str) -> str:
|
||||
return substr
|
||||
|
||||
def get_repl_substr_for_matching(self, substr: str) -> str:
|
||||
return substr if substr.startswith("\\") else ""
|
||||
|
||||
def get_specified_items(
|
||||
self, cmd_span_pairs: list[tuple[Span, Span]]
|
||||
) -> list[tuple[Span, dict[str, str]]]:
|
||||
cmd_content_spans = [
|
||||
(span_begin, span_end)
|
||||
for (_, span_begin), (span_end, _) in cmd_span_pairs
|
||||
]
|
||||
specified_spans = [
|
||||
*[
|
||||
cmd_content_spans[range_begin]
|
||||
for _, (range_begin, range_end) in self.compress_neighbours([
|
||||
(span_begin + index, span_end - index)
|
||||
for index, (span_begin, span_end) in enumerate(
|
||||
cmd_content_spans
|
||||
)
|
||||
])
|
||||
if range_end - range_begin >= 2
|
||||
],
|
||||
*[
|
||||
span
|
||||
for selector in self.tex_to_color_map
|
||||
for span in self.find_spans_by_selector(selector)
|
||||
],
|
||||
*self.find_spans_by_selector(self.isolate)
|
||||
]
|
||||
return [(span, {}) for span in specified_spans]
|
||||
@staticmethod
|
||||
def get_command_matches(string: str) -> list[re.Match]:
|
||||
# Lump together adjacent brace pairs
|
||||
pattern = re.compile(r"""
|
||||
(?P<command>\\(?:[a-zA-Z]+|.))
|
||||
|(?P<open>{+)
|
||||
|(?P<close>}+)
|
||||
""", flags=re.X | re.S)
|
||||
result = []
|
||||
open_stack = []
|
||||
for match_obj in pattern.finditer(string):
|
||||
if match_obj.group("open"):
|
||||
open_stack.append((match_obj.span(), len(result)))
|
||||
elif match_obj.group("close"):
|
||||
close_start, close_end = match_obj.span()
|
||||
while True:
|
||||
if not open_stack:
|
||||
raise ValueError("Missing '{' inserted")
|
||||
(open_start, open_end), index = open_stack.pop()
|
||||
n = min(open_end - open_start, close_end - close_start)
|
||||
result.insert(index, pattern.fullmatch(
|
||||
string, pos=open_end - n, endpos=open_end
|
||||
))
|
||||
result.append(pattern.fullmatch(
|
||||
string, pos=close_start, endpos=close_start + n
|
||||
))
|
||||
close_start += n
|
||||
if close_start < close_end:
|
||||
continue
|
||||
open_end -= n
|
||||
if open_start < open_end:
|
||||
open_stack.append(((open_start, open_end), index))
|
||||
break
|
||||
else:
|
||||
result.append(match_obj)
|
||||
if open_stack:
|
||||
raise ValueError("Missing '}' inserted")
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_color_cmd_str(rgb_hex: str) -> str:
|
||||
def get_command_flag(match_obj: re.Match) -> int:
|
||||
if match_obj.group("open"):
|
||||
return 1
|
||||
if match_obj.group("close"):
|
||||
return -1
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def replace_for_content(match_obj: re.Match) -> str:
|
||||
return match_obj.group()
|
||||
|
||||
@staticmethod
|
||||
def replace_for_matching(match_obj: re.Match) -> str:
|
||||
if match_obj.group("command"):
|
||||
return match_obj.group()
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_attr_dict_from_command_pair(
|
||||
open_command: re.Match, close_command: re.Match
|
||||
) -> dict[str, str] | None:
|
||||
if len(open_command.group()) >= 2:
|
||||
return {}
|
||||
return None
|
||||
|
||||
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
|
||||
return [
|
||||
(span, {})
|
||||
for selector in self.tex_to_color_map
|
||||
for span in self.find_spans_by_selector(selector)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_color_command(rgb_hex: str) -> str:
|
||||
rgb = MTex.hex_to_int(rgb_hex)
|
||||
rg, b = divmod(rgb, 256)
|
||||
r, g = divmod(rg, 256)
|
||||
return f"\\color[RGB]{{{r}, {g}, {b}}}"
|
||||
|
||||
@staticmethod
|
||||
def get_cmd_str_pair(
|
||||
attr_dict: dict[str, str], label_hex: str | None
|
||||
) -> tuple[str, str]:
|
||||
def get_command_string(
|
||||
attr_dict: dict[str, str], is_end: bool, label_hex: str | None
|
||||
) -> str:
|
||||
if label_hex is None:
|
||||
return "", ""
|
||||
return "{{" + MTex.get_color_cmd_str(label_hex), "}}"
|
||||
return ""
|
||||
if is_end:
|
||||
return "}}"
|
||||
return "{{" + MTex.get_color_command(label_hex)
|
||||
|
||||
def get_content_prefix_and_suffix(
|
||||
self, is_labelled: bool
|
||||
@ -135,17 +173,14 @@ class MTex(StringMobject):
|
||||
prefix_lines = []
|
||||
suffix_lines = []
|
||||
if not is_labelled:
|
||||
prefix_lines.append(self.get_color_cmd_str(self.base_color_hex))
|
||||
prefix_lines.append(self.get_color_command(
|
||||
self.color_to_hex(self.base_color)
|
||||
))
|
||||
if self.alignment:
|
||||
prefix_lines.append(self.alignment)
|
||||
if self.tex_environment:
|
||||
if isinstance(self.tex_environment, str):
|
||||
env_prefix = f"\\begin{{{self.tex_environment}}}"
|
||||
env_suffix = f"\\end{{{self.tex_environment}}}"
|
||||
else:
|
||||
env_prefix, env_suffix = self.tex_environment
|
||||
prefix_lines.append(env_prefix)
|
||||
suffix_lines.append(env_suffix)
|
||||
prefix_lines.append(f"\\begin{{{self.tex_environment}}}")
|
||||
suffix_lines.append(f"\\end{{{self.tex_environment}}}")
|
||||
return (
|
||||
"".join([line + "\n" for line in prefix_lines]),
|
||||
"".join(["\n" + line for line in suffix_lines])
|
||||
@ -156,8 +191,8 @@ class MTex(StringMobject):
|
||||
def get_parts_by_tex(self, selector: Selector) -> VGroup:
|
||||
return self.select_parts(selector)
|
||||
|
||||
def get_part_by_tex(self, selector: Selector) -> VGroup:
|
||||
return self.select_part(selector)
|
||||
def get_part_by_tex(self, selector: Selector, **kwargs) -> VGroup:
|
||||
return self.select_part(selector, **kwargs)
|
||||
|
||||
def set_color_by_tex(self, selector: Selector, color: ManimColor):
|
||||
return self.set_parts_color(selector, color)
|
||||
|
@ -18,7 +18,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from colour import Color
|
||||
from typing import Iterable, Sequence, TypeVar, Union
|
||||
from typing import Callable, Iterable, Union
|
||||
|
||||
ManimColor = Union[str, Color]
|
||||
Span = tuple[int, int]
|
||||
@ -32,7 +32,6 @@ if TYPE_CHECKING:
|
||||
tuple[Union[int, None], Union[int, None]]
|
||||
]]
|
||||
]
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class StringMobject(SVGMobject, ABC):
|
||||
@ -47,7 +46,7 @@ class StringMobject(SVGMobject, ABC):
|
||||
if they want to do anything with their corresponding submobjects.
|
||||
`isolate` parameter can be either a string, a `re.Pattern` object,
|
||||
or a 2-tuple containing integers or None, or a collection of the above.
|
||||
Note, substrings specified cannot *partially* overlap with each other.
|
||||
Note, substrings specified cannot *partly* overlap with each other.
|
||||
|
||||
Each instance of `StringMobject` generates 2 svg files.
|
||||
The additional one is generated with some color commands inserted,
|
||||
@ -64,6 +63,7 @@ class StringMobject(SVGMobject, ABC):
|
||||
},
|
||||
"base_color": WHITE,
|
||||
"isolate": (),
|
||||
"protect": (),
|
||||
}
|
||||
|
||||
def __init__(self, string: str, **kwargs):
|
||||
@ -71,9 +71,7 @@ class StringMobject(SVGMobject, ABC):
|
||||
digest_config(self, kwargs)
|
||||
if self.base_color is None:
|
||||
self.base_color = WHITE
|
||||
self.base_color_hex = self.color_to_hex(self.base_color)
|
||||
|
||||
self.full_span = (0, len(self.string))
|
||||
self.parse()
|
||||
super().__init__(**kwargs)
|
||||
self.labels = [submob.label for submob in self.submobjects]
|
||||
@ -90,9 +88,9 @@ class StringMobject(SVGMobject, ABC):
|
||||
super().generate_mobject()
|
||||
|
||||
labels_count = len(self.labelled_spans)
|
||||
if not labels_count:
|
||||
if labels_count == 1:
|
||||
for submob in self.submobjects:
|
||||
submob.label = -1
|
||||
submob.label = 0
|
||||
return
|
||||
|
||||
labelled_content = self.get_content(is_labelled=True)
|
||||
@ -104,7 +102,7 @@ class StringMobject(SVGMobject, ABC):
|
||||
"to the original svg. Skip the labelling process."
|
||||
)
|
||||
for submob in self.submobjects:
|
||||
submob.label = -1
|
||||
submob.label = 0
|
||||
return
|
||||
|
||||
self.rearrange_submobjects_by_positions(labelled_svg)
|
||||
@ -112,18 +110,21 @@ class StringMobject(SVGMobject, ABC):
|
||||
for submob, labelled_svg_submob in zip(
|
||||
self.submobjects, labelled_svg.submobjects
|
||||
):
|
||||
color_int = self.hex_to_int(self.color_to_hex(
|
||||
label = self.hex_to_int(self.color_to_hex(
|
||||
labelled_svg_submob.get_fill_color()
|
||||
))
|
||||
if color_int > labels_count:
|
||||
unrecognizable_colors.append(color_int)
|
||||
color_int = 0
|
||||
submob.label = color_int - 1
|
||||
if label >= labels_count:
|
||||
unrecognizable_colors.append(label)
|
||||
label = 0
|
||||
submob.label = label
|
||||
if unrecognizable_colors:
|
||||
log.warning(
|
||||
"Unrecognizable color labels detected (%s, etc). "
|
||||
"Unrecognizable color labels detected (%s). "
|
||||
"The result could be unexpected.",
|
||||
self.int_to_hex(unrecognizable_colors[0])
|
||||
", ".join(
|
||||
self.int_to_hex(color)
|
||||
for color in unrecognizable_colors
|
||||
)
|
||||
)
|
||||
|
||||
def rearrange_submobjects_by_positions(
|
||||
@ -153,30 +154,27 @@ class StringMobject(SVGMobject, ABC):
|
||||
|
||||
# Toolkits
|
||||
|
||||
def get_substr(self, span: Span) -> str:
|
||||
return self.string[slice(*span)]
|
||||
|
||||
def find_spans(self, pattern: str | re.Pattern) -> list[Span]:
|
||||
return [
|
||||
match_obj.span()
|
||||
for match_obj in re.finditer(pattern, self.string)
|
||||
]
|
||||
|
||||
def find_spans_by_selector(self, selector: Selector) -> list[Span]:
|
||||
def find_spans_by_single_selector(sel):
|
||||
if isinstance(sel, str):
|
||||
return self.find_spans(re.escape(sel))
|
||||
return [
|
||||
match_obj.span()
|
||||
for match_obj in re.finditer(re.escape(sel), self.string)
|
||||
]
|
||||
if isinstance(sel, re.Pattern):
|
||||
return self.find_spans(sel)
|
||||
return [
|
||||
match_obj.span()
|
||||
for match_obj in sel.finditer(self.string)
|
||||
]
|
||||
if isinstance(sel, tuple) and len(sel) == 2 and all(
|
||||
isinstance(index, int) or index is None
|
||||
for index in sel
|
||||
):
|
||||
l = self.full_span[1]
|
||||
l = len(self.string)
|
||||
span = tuple(
|
||||
default_index if index is None else
|
||||
min(index, l) if index >= 0 else max(index + l, 0)
|
||||
for index, default_index in zip(sel, self.full_span)
|
||||
for index, default_index in zip(sel, (0, l))
|
||||
)
|
||||
return [span]
|
||||
return None
|
||||
@ -189,57 +187,12 @@ class StringMobject(SVGMobject, ABC):
|
||||
if spans is None:
|
||||
raise TypeError(f"Invalid selector: '{sel}'")
|
||||
result.extend(spans)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_neighbouring_pairs(vals: Sequence[T]) -> list[tuple[T, T]]:
|
||||
return list(zip(vals[:-1], vals[1:]))
|
||||
|
||||
@staticmethod
|
||||
def compress_neighbours(vals: Sequence[T]) -> list[tuple[T, Span]]:
|
||||
if not vals:
|
||||
return []
|
||||
|
||||
unique_vals = [vals[0]]
|
||||
indices = [0]
|
||||
for index, val in enumerate(vals):
|
||||
if val == unique_vals[-1]:
|
||||
continue
|
||||
unique_vals.append(val)
|
||||
indices.append(index)
|
||||
indices.append(len(vals))
|
||||
val_ranges = StringMobject.get_neighbouring_pairs(indices)
|
||||
return list(zip(unique_vals, val_ranges))
|
||||
return list(filter(lambda span: span[0] <= span[1], result))
|
||||
|
||||
@staticmethod
|
||||
def span_contains(span_0: Span, span_1: Span) -> bool:
|
||||
return span_0[0] <= span_1[0] and span_0[1] >= span_1[1]
|
||||
|
||||
@staticmethod
|
||||
def get_complement_spans(
|
||||
universal_span: Span, interval_spans: list[Span]
|
||||
) -> list[Span]:
|
||||
if not interval_spans:
|
||||
return [universal_span]
|
||||
|
||||
span_ends, span_begins = zip(*interval_spans)
|
||||
return list(zip(
|
||||
(universal_span[0], *span_begins),
|
||||
(*span_ends, universal_span[1])
|
||||
))
|
||||
|
||||
def replace_substr(self, span: Span, repl_items: list[Span, str]):
|
||||
if not repl_items:
|
||||
return self.get_substr(span)
|
||||
|
||||
repl_spans, repl_strs = zip(*sorted(repl_items, key=lambda t: t[0]))
|
||||
pieces = [
|
||||
self.get_substr(piece_span)
|
||||
for piece_span in self.get_complement_spans(span, repl_spans)
|
||||
]
|
||||
repl_strs = [*repl_strs, ""]
|
||||
return "".join(it.chain(*zip(pieces, repl_strs)))
|
||||
|
||||
@staticmethod
|
||||
def color_to_hex(color: ManimColor) -> str:
|
||||
return rgb_to_hex(color_to_rgb(color))
|
||||
@ -255,131 +208,220 @@ class StringMobject(SVGMobject, ABC):
|
||||
# Parsing
|
||||
|
||||
def parse(self) -> None:
|
||||
cmd_spans = self.get_cmd_spans()
|
||||
cmd_substrs = [self.get_substr(span) for span in cmd_spans]
|
||||
flags = [self.get_substr_flag(substr) for substr in cmd_substrs]
|
||||
specified_items = self.get_specified_items(
|
||||
self.get_cmd_span_pairs(cmd_spans, flags)
|
||||
)
|
||||
split_items = [
|
||||
(span, attr_dict)
|
||||
for specified_span, attr_dict in specified_items
|
||||
for span in self.split_span_by_levels(
|
||||
specified_span, cmd_spans, flags
|
||||
def get_substr(span: Span) -> str:
|
||||
return self.string[slice(*span)]
|
||||
|
||||
configured_items = self.get_configured_items()
|
||||
isolated_spans = self.find_spans_by_selector(self.isolate)
|
||||
protected_spans = self.find_spans_by_selector(self.protect)
|
||||
command_matches = self.get_command_matches(self.string)
|
||||
|
||||
def get_key(category, i, flag):
|
||||
def get_span_by_category(category, i):
|
||||
if category == 0:
|
||||
return configured_items[i][0]
|
||||
if category == 1:
|
||||
return isolated_spans[i]
|
||||
if category == 2:
|
||||
return protected_spans[i]
|
||||
return command_matches[i].span()
|
||||
|
||||
index, paired_index = get_span_by_category(category, i)[::flag]
|
||||
return (
|
||||
index,
|
||||
flag * (2 if index != paired_index else -1),
|
||||
-paired_index,
|
||||
flag * category,
|
||||
flag * i
|
||||
)
|
||||
]
|
||||
|
||||
self.specified_spans = [span for span, _ in specified_items]
|
||||
self.split_items = split_items
|
||||
self.labelled_spans = [span for span, _ in split_items]
|
||||
self.cmd_repl_items_for_content = [
|
||||
(span, self.get_repl_substr_for_content(substr))
|
||||
for span, substr in zip(cmd_spans, cmd_substrs)
|
||||
]
|
||||
self.cmd_repl_items_for_matching = [
|
||||
(span, self.get_repl_substr_for_matching(substr))
|
||||
for span, substr in zip(cmd_spans, cmd_substrs)
|
||||
]
|
||||
self.check_overlapping()
|
||||
index_items = sorted([
|
||||
(category, i, flag)
|
||||
for category, item_length in enumerate((
|
||||
len(configured_items),
|
||||
len(isolated_spans),
|
||||
len(protected_spans),
|
||||
len(command_matches)
|
||||
))
|
||||
for i in range(item_length)
|
||||
for flag in (1, -1)
|
||||
], key=lambda t: get_key(*t))
|
||||
|
||||
inserted_items = []
|
||||
labelled_items = []
|
||||
overlapping_spans = []
|
||||
level_mismatched_spans = []
|
||||
|
||||
label = 1
|
||||
protect_level = 0
|
||||
bracket_stack = [0]
|
||||
bracket_count = 0
|
||||
open_command_stack = []
|
||||
open_stack = []
|
||||
for category, i, flag in index_items:
|
||||
if category >= 2:
|
||||
protect_level += flag
|
||||
if flag == 1 or category == 2:
|
||||
continue
|
||||
inserted_items.append((i, 0))
|
||||
command_match = command_matches[i]
|
||||
command_flag = self.get_command_flag(command_match)
|
||||
if command_flag == 1:
|
||||
bracket_count += 1
|
||||
bracket_stack.append(bracket_count)
|
||||
open_command_stack.append((len(inserted_items), i))
|
||||
continue
|
||||
if command_flag == 0:
|
||||
continue
|
||||
pos, i_ = open_command_stack.pop()
|
||||
bracket_stack.pop()
|
||||
open_command_match = command_matches[i_]
|
||||
attr_dict = self.get_attr_dict_from_command_pair(
|
||||
open_command_match, command_match
|
||||
)
|
||||
if attr_dict is None:
|
||||
continue
|
||||
span = (open_command_match.end(), command_match.start())
|
||||
labelled_items.append((span, attr_dict))
|
||||
inserted_items.insert(pos, (label, 1))
|
||||
inserted_items.insert(-1, (label, -1))
|
||||
label += 1
|
||||
continue
|
||||
if flag == 1:
|
||||
open_stack.append((
|
||||
len(inserted_items), category, i,
|
||||
protect_level, bracket_stack.copy()
|
||||
))
|
||||
continue
|
||||
span, attr_dict = configured_items[i] \
|
||||
if category == 0 else (isolated_spans[i], {})
|
||||
pos, category_, i_, protect_level_, bracket_stack_ \
|
||||
= open_stack.pop()
|
||||
if category_ != category or i_ != i:
|
||||
overlapping_spans.append(span)
|
||||
continue
|
||||
if protect_level_ or protect_level:
|
||||
continue
|
||||
if bracket_stack_ != bracket_stack:
|
||||
level_mismatched_spans.append(span)
|
||||
continue
|
||||
labelled_items.append((span, attr_dict))
|
||||
inserted_items.insert(pos, (label, 1))
|
||||
inserted_items.append((label, -1))
|
||||
label += 1
|
||||
labelled_items.insert(0, ((0, len(self.string)), {}))
|
||||
inserted_items.insert(0, (0, 1))
|
||||
inserted_items.append((0, -1))
|
||||
|
||||
if overlapping_spans:
|
||||
log.warning(
|
||||
"Partly overlapping substrings detected: %s",
|
||||
", ".join(
|
||||
f"'{get_substr(span)}'"
|
||||
for span in overlapping_spans
|
||||
)
|
||||
)
|
||||
if level_mismatched_spans:
|
||||
log.warning(
|
||||
"Cannot handle substrings: %s",
|
||||
", ".join(
|
||||
f"'{get_substr(span)}'"
|
||||
for span in level_mismatched_spans
|
||||
)
|
||||
)
|
||||
|
||||
def reconstruct_string(
|
||||
start_item: tuple[int, int],
|
||||
end_item: tuple[int, int],
|
||||
command_replace_func: Callable[[re.Match], str],
|
||||
command_insert_func: Callable[[int, int, dict[str, str]], str]
|
||||
) -> str:
|
||||
def get_edge_item(i: int, flag: int) -> tuple[Span, str]:
|
||||
if flag == 0:
|
||||
match_obj = command_matches[i]
|
||||
return (
|
||||
match_obj.span(),
|
||||
command_replace_func(match_obj)
|
||||
)
|
||||
span, attr_dict = labelled_items[i]
|
||||
index = span[flag < 0]
|
||||
return (
|
||||
(index, index),
|
||||
command_insert_func(i, flag, attr_dict)
|
||||
)
|
||||
|
||||
items = [
|
||||
get_edge_item(i, flag)
|
||||
for i, flag in inserted_items[slice(
|
||||
inserted_items.index(start_item),
|
||||
inserted_items.index(end_item) + 1
|
||||
)]
|
||||
]
|
||||
pieces = [
|
||||
get_substr((start, end))
|
||||
for start, end in zip(
|
||||
[interval_end for (_, interval_end), _ in items[:-1]],
|
||||
[interval_start for (interval_start, _), _ in items[1:]]
|
||||
)
|
||||
]
|
||||
interval_pieces = [piece for _, piece in items[1:-1]]
|
||||
return "".join(it.chain(*zip(pieces, (*interval_pieces, ""))))
|
||||
|
||||
self.labelled_spans = [span for span, _ in labelled_items]
|
||||
self.reconstruct_string = reconstruct_string
|
||||
|
||||
def get_content(self, is_labelled: bool) -> str:
|
||||
content = self.reconstruct_string(
|
||||
(0, 1), (0, -1),
|
||||
self.replace_for_content,
|
||||
lambda label, flag, attr_dict: self.get_command_string(
|
||||
attr_dict,
|
||||
is_end=flag < 0,
|
||||
label_hex=self.int_to_hex(label) if is_labelled else None
|
||||
)
|
||||
)
|
||||
prefix, suffix = self.get_content_prefix_and_suffix(
|
||||
is_labelled=is_labelled
|
||||
)
|
||||
return "".join((prefix, content, suffix))
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_cmd_spans(self) -> list[Span]:
|
||||
def get_command_matches(string: str) -> list[re.Match]:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_substr_flag(self, substr: str) -> int:
|
||||
def get_command_flag(match_obj: re.Match) -> int:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_repl_substr_for_content(self, substr: str) -> str:
|
||||
return ""
|
||||
|
||||
@abstractmethod
|
||||
def get_repl_substr_for_matching(self, substr: str) -> str:
|
||||
def replace_for_content(match_obj: re.Match) -> str:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_cmd_span_pairs(
|
||||
cmd_spans: list[Span], flags: list[int]
|
||||
) -> list[tuple[Span, Span]]:
|
||||
result = []
|
||||
begin_cmd_spans_stack = []
|
||||
for cmd_span, flag in zip(cmd_spans, flags):
|
||||
if flag == 1:
|
||||
begin_cmd_spans_stack.append(cmd_span)
|
||||
elif flag == -1:
|
||||
if not begin_cmd_spans_stack:
|
||||
raise ValueError("Missing open command")
|
||||
begin_cmd_span = begin_cmd_spans_stack.pop()
|
||||
result.append((begin_cmd_span, cmd_span))
|
||||
if begin_cmd_spans_stack:
|
||||
raise ValueError("Missing close command")
|
||||
return result
|
||||
@abstractmethod
|
||||
def replace_for_matching(match_obj: re.Match) -> str:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_attr_dict_from_command_pair(
|
||||
open_command: re.Match, close_command: re.Match,
|
||||
) -> dict[str, str] | None:
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def get_specified_items(
|
||||
self, cmd_span_pairs: list[tuple[Span, Span]]
|
||||
) -> list[tuple[Span, dict[str, str]]]:
|
||||
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
|
||||
return []
|
||||
|
||||
def split_span_by_levels(
|
||||
self, arbitrary_span: Span, cmd_spans: list[Span], flags: list[int]
|
||||
) -> list[Span]:
|
||||
cmd_range = (
|
||||
sum([
|
||||
arbitrary_span[0] > interval_begin
|
||||
for interval_begin, _ in cmd_spans
|
||||
]),
|
||||
sum([
|
||||
arbitrary_span[1] >= interval_end
|
||||
for _, interval_end in cmd_spans
|
||||
])
|
||||
)
|
||||
complement_spans = self.get_complement_spans(
|
||||
self.full_span, cmd_spans
|
||||
)
|
||||
adjusted_span = (
|
||||
max(arbitrary_span[0], complement_spans[cmd_range[0]][0]),
|
||||
min(arbitrary_span[1], complement_spans[cmd_range[1]][1])
|
||||
)
|
||||
if adjusted_span[0] > adjusted_span[1]:
|
||||
return []
|
||||
|
||||
upward_cmd_spans = []
|
||||
downward_cmd_spans = []
|
||||
for cmd_span, flag in list(zip(cmd_spans, flags))[slice(*cmd_range)]:
|
||||
if flag == 1:
|
||||
upward_cmd_spans.append(cmd_span)
|
||||
elif flag == -1:
|
||||
if upward_cmd_spans:
|
||||
upward_cmd_spans.pop()
|
||||
else:
|
||||
downward_cmd_spans.append(cmd_span)
|
||||
return list(filter(
|
||||
lambda span: self.get_substr(span).strip(),
|
||||
self.get_complement_spans(
|
||||
adjusted_span, downward_cmd_spans + upward_cmd_spans
|
||||
)
|
||||
))
|
||||
|
||||
def check_overlapping(self) -> None:
|
||||
labelled_spans = self.labelled_spans
|
||||
if len(labelled_spans) >= 16777216:
|
||||
raise ValueError("Cannot handle that many substrings")
|
||||
for span_0, span_1 in it.product(labelled_spans, repeat=2):
|
||||
if not span_0[0] < span_1[0] < span_0[1] < span_1[1]:
|
||||
continue
|
||||
raise ValueError(
|
||||
"Partially overlapping substrings detected: "
|
||||
f"'{self.get_substr(span_0)}' and '{self.get_substr(span_1)}'"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_cmd_str_pair(
|
||||
attr_dict: dict[str, str], label_hex: str | None
|
||||
) -> tuple[str, str]:
|
||||
return "", ""
|
||||
def get_command_string(
|
||||
attr_dict: dict[str, str], is_end: bool, label_hex: str | None
|
||||
) -> str:
|
||||
return ""
|
||||
|
||||
@abstractmethod
|
||||
def get_content_prefix_and_suffix(
|
||||
@ -387,38 +429,6 @@ class StringMobject(SVGMobject, ABC):
|
||||
) -> tuple[str, str]:
|
||||
return "", ""
|
||||
|
||||
def get_content(self, is_labelled: bool) -> str:
|
||||
inserted_str_pairs = [
|
||||
(span, self.get_cmd_str_pair(
|
||||
attr_dict,
|
||||
label_hex=self.int_to_hex(label + 1) if is_labelled else None
|
||||
))
|
||||
for label, (span, attr_dict) in enumerate(self.split_items)
|
||||
]
|
||||
inserted_str_items = sorted([
|
||||
(index, s)
|
||||
for (index, _), s in [
|
||||
*sorted([
|
||||
(span[::-1], end_str)
|
||||
for span, (_, end_str) in reversed(inserted_str_pairs)
|
||||
], key=lambda t: (t[0][0], -t[0][1])),
|
||||
*sorted([
|
||||
(span, begin_str)
|
||||
for span, (begin_str, _) in inserted_str_pairs
|
||||
], key=lambda t: (t[0][0], -t[0][1]))
|
||||
]
|
||||
], key=lambda t: t[0])
|
||||
repl_items = self.cmd_repl_items_for_content + [
|
||||
((index, index), inserted_str)
|
||||
for index, inserted_str in inserted_str_items
|
||||
]
|
||||
prefix, suffix = self.get_content_prefix_and_suffix(is_labelled)
|
||||
return "".join([
|
||||
prefix,
|
||||
self.replace_substr(self.full_span, repl_items),
|
||||
suffix
|
||||
])
|
||||
|
||||
# Selector
|
||||
|
||||
def get_submob_indices_list_by_span(
|
||||
@ -427,59 +437,69 @@ class StringMobject(SVGMobject, ABC):
|
||||
return [
|
||||
submob_index
|
||||
for submob_index, label in enumerate(self.labels)
|
||||
if label != -1 and self.span_contains(
|
||||
arbitrary_span, self.labelled_spans[label]
|
||||
)
|
||||
if self.span_contains(arbitrary_span, self.labelled_spans[label])
|
||||
]
|
||||
|
||||
def get_specified_part_items(self) -> list[tuple[str, list[int]]]:
|
||||
return [
|
||||
(
|
||||
self.get_substr(span),
|
||||
self.string[slice(*span)],
|
||||
self.get_submob_indices_list_by_span(span)
|
||||
)
|
||||
for span in self.specified_spans
|
||||
for span in self.labelled_spans[1:]
|
||||
]
|
||||
|
||||
def get_group_part_items(self) -> list[tuple[str, list[int]]]:
|
||||
if not self.labels:
|
||||
return []
|
||||
|
||||
group_labels, labelled_submob_ranges = zip(
|
||||
*self.compress_neighbours(self.labels)
|
||||
)
|
||||
ordered_spans = [
|
||||
self.labelled_spans[label] if label != -1 else self.full_span
|
||||
for label in group_labels
|
||||
]
|
||||
interval_spans = [
|
||||
(
|
||||
next_span[0]
|
||||
if self.span_contains(prev_span, next_span)
|
||||
else prev_span[1],
|
||||
prev_span[1]
|
||||
if self.span_contains(next_span, prev_span)
|
||||
else next_span[0]
|
||||
)
|
||||
for prev_span, next_span in self.get_neighbouring_pairs(
|
||||
ordered_spans
|
||||
)
|
||||
]
|
||||
group_substrs = [
|
||||
re.sub(r"\s+", "", self.replace_substr(
|
||||
span, [
|
||||
(cmd_span, repl_str)
|
||||
for cmd_span, repl_str in self.cmd_repl_items_for_matching
|
||||
if self.span_contains(span, cmd_span)
|
||||
]
|
||||
))
|
||||
for span in self.get_complement_spans(
|
||||
(ordered_spans[0][0], ordered_spans[-1][1]), interval_spans
|
||||
)
|
||||
]
|
||||
def get_neighbouring_pairs(vals):
|
||||
return list(zip(vals[:-1], vals[1:]))
|
||||
|
||||
range_lens, group_labels = zip(*(
|
||||
(len(list(grouper)), val)
|
||||
for val, grouper in it.groupby(self.labels)
|
||||
))
|
||||
submob_indices_lists = [
|
||||
list(range(*submob_range))
|
||||
for submob_range in labelled_submob_ranges
|
||||
for submob_range in get_neighbouring_pairs(
|
||||
[0, *it.accumulate(range_lens)]
|
||||
)
|
||||
]
|
||||
labelled_spans = self.labelled_spans
|
||||
start_items = [
|
||||
(group_labels[0], 1),
|
||||
*(
|
||||
(curr_label, 1)
|
||||
if self.span_contains(
|
||||
labelled_spans[prev_label], labelled_spans[curr_label]
|
||||
)
|
||||
else (prev_label, -1)
|
||||
for prev_label, curr_label in get_neighbouring_pairs(
|
||||
group_labels
|
||||
)
|
||||
)
|
||||
]
|
||||
end_items = [
|
||||
*(
|
||||
(curr_label, -1)
|
||||
if self.span_contains(
|
||||
labelled_spans[next_label], labelled_spans[curr_label]
|
||||
)
|
||||
else (next_label, 1)
|
||||
for curr_label, next_label in get_neighbouring_pairs(
|
||||
group_labels
|
||||
)
|
||||
),
|
||||
(group_labels[-1], -1)
|
||||
]
|
||||
group_substrs = [
|
||||
re.sub(r"\s+", "", self.reconstruct_string(
|
||||
start_item, end_item,
|
||||
self.replace_for_matching,
|
||||
lambda label, flag, attr_dict: ""
|
||||
))
|
||||
for start_item, end_item in zip(start_items, end_items)
|
||||
]
|
||||
return list(zip(group_substrs, submob_indices_lists))
|
||||
|
||||
@ -497,13 +517,13 @@ class StringMobject(SVGMobject, ABC):
|
||||
def build_parts_from_indices_lists(
|
||||
self, indices_lists: list[list[int]]
|
||||
) -> VGroup:
|
||||
return VGroup(*[
|
||||
VGroup(*[
|
||||
return VGroup(*(
|
||||
VGroup(*(
|
||||
self.submobjects[submob_index]
|
||||
for submob_index in indices_list
|
||||
])
|
||||
))
|
||||
for indices_list in indices_lists
|
||||
])
|
||||
))
|
||||
|
||||
def build_groups(self) -> VGroup:
|
||||
return self.build_parts_from_indices_lists([
|
||||
|
@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
@ -19,6 +18,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.directories import get_mobject_data_dir
|
||||
from manimlib.utils.images import get_full_vector_image_path
|
||||
from manimlib.utils.iterables import hash_obj
|
||||
from manimlib.utils.simple_functions import hash_string
|
||||
|
||||
|
||||
SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {}
|
||||
@ -106,7 +106,7 @@ class SVGMobject(VMobject):
|
||||
return get_full_vector_image_path(self.file_name)
|
||||
|
||||
def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree:
|
||||
config_style_dict = self.generate_config_style_dict()
|
||||
config_style_attrs = self.generate_config_style_dict()
|
||||
style_keys = (
|
||||
"fill",
|
||||
"fill-opacity",
|
||||
@ -116,14 +116,17 @@ class SVGMobject(VMobject):
|
||||
"style"
|
||||
)
|
||||
root = element_tree.getroot()
|
||||
root_style_dict = {
|
||||
k: v for k, v in root.attrib.items()
|
||||
style_attrs = {
|
||||
k: v
|
||||
for k, v in root.attrib.items()
|
||||
if k in style_keys
|
||||
}
|
||||
|
||||
new_root = ET.Element("svg", {})
|
||||
config_style_node = ET.SubElement(new_root, "g", config_style_dict)
|
||||
root_style_node = ET.SubElement(config_style_node, "g", root_style_dict)
|
||||
# Ignore other attributes in case that svgelements cannot parse them
|
||||
SVG_XMLNS = "{http://www.w3.org/2000/svg}"
|
||||
new_root = ET.Element("svg")
|
||||
config_style_node = ET.SubElement(new_root, f"{SVG_XMLNS}g", config_style_attrs)
|
||||
root_style_node = ET.SubElement(config_style_node, f"{SVG_XMLNS}g", style_attrs)
|
||||
root_style_node.extend(root)
|
||||
return ET.ElementTree(new_root)
|
||||
|
||||
@ -147,7 +150,7 @@ class SVGMobject(VMobject):
|
||||
def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]:
|
||||
result = []
|
||||
for shape in svg.elements():
|
||||
if isinstance(shape, se.Group):
|
||||
if isinstance(shape, (se.Group, se.Use)):
|
||||
continue
|
||||
elif isinstance(shape, se.Path):
|
||||
mob = self.path_to_mobject(shape)
|
||||
@ -155,9 +158,7 @@ class SVGMobject(VMobject):
|
||||
mob = self.line_to_mobject(shape)
|
||||
elif isinstance(shape, se.Rect):
|
||||
mob = self.rect_to_mobject(shape)
|
||||
elif isinstance(shape, se.Circle):
|
||||
mob = self.circle_to_mobject(shape)
|
||||
elif isinstance(shape, se.Ellipse):
|
||||
elif isinstance(shape, (se.Circle, se.Ellipse)):
|
||||
mob = self.ellipse_to_mobject(shape)
|
||||
elif isinstance(shape, se.Polygon):
|
||||
mob = self.polygon_to_mobject(shape)
|
||||
@ -168,11 +169,12 @@ class SVGMobject(VMobject):
|
||||
elif type(shape) == se.SVGElement:
|
||||
continue
|
||||
else:
|
||||
log.warning(f"Unsupported element type: {type(shape)}")
|
||||
log.warning("Unsupported element type: %s", type(shape))
|
||||
continue
|
||||
if not mob.has_points():
|
||||
continue
|
||||
self.apply_style_to_mobject(mob, shape)
|
||||
if isinstance(shape, se.GraphicObject):
|
||||
self.apply_style_to_mobject(mob, shape)
|
||||
if isinstance(shape, se.Transformable) and shape.apply:
|
||||
self.handle_transform(mob, shape.transform)
|
||||
result.append(mob)
|
||||
@ -203,21 +205,10 @@ class SVGMobject(VMobject):
|
||||
)
|
||||
return mob
|
||||
|
||||
@staticmethod
|
||||
def handle_transform(mob, matrix):
|
||||
mat = np.array([
|
||||
[matrix.a, matrix.c],
|
||||
[matrix.b, matrix.d]
|
||||
])
|
||||
vec = np.array([matrix.e, matrix.f, 0.0])
|
||||
mob.apply_matrix(mat)
|
||||
mob.shift(vec)
|
||||
return mob
|
||||
|
||||
def path_to_mobject(self, path: se.Path) -> VMobjectFromSVGPath:
|
||||
return VMobjectFromSVGPath(path, **self.path_string_config)
|
||||
|
||||
def line_to_mobject(self, line: se.Line) -> Line:
|
||||
def line_to_mobject(self, line: se.SimpleLine) -> Line:
|
||||
return Line(
|
||||
start=_convert_point_to_3d(line.x1, line.y1),
|
||||
end=_convert_point_to_3d(line.x2, line.y2)
|
||||
@ -242,15 +233,7 @@ class SVGMobject(VMobject):
|
||||
))
|
||||
return mob
|
||||
|
||||
def circle_to_mobject(self, circle: se.Circle) -> Circle:
|
||||
# svgelements supports `rx` & `ry` but `r`
|
||||
mob = Circle(radius=circle.rx)
|
||||
mob.shift(_convert_point_to_3d(
|
||||
circle.cx, circle.cy
|
||||
))
|
||||
return mob
|
||||
|
||||
def ellipse_to_mobject(self, ellipse: se.Ellipse) -> Circle:
|
||||
def ellipse_to_mobject(self, ellipse: se.Circle | se.Ellipse) -> Circle:
|
||||
mob = Circle(radius=ellipse.rx)
|
||||
mob.stretch_to_fit_height(2 * ellipse.ry)
|
||||
mob.shift(_convert_point_to_3d(
|
||||
@ -302,8 +285,7 @@ class VMobjectFromSVGPath(VMobject):
|
||||
# will be saved to a file so that future calls for the same path
|
||||
# don't need to retrace the same computation.
|
||||
path_string = self.path_obj.d()
|
||||
hasher = hashlib.sha256(path_string.encode())
|
||||
path_hash = hasher.hexdigest()[:16]
|
||||
path_hash = hash_string(path_string)
|
||||
points_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_points.npy")
|
||||
tris_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_tris.npy")
|
||||
|
||||
|
@ -13,8 +13,7 @@ from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.tex_file_writing import display_during_execution
|
||||
from manimlib.utils.tex_file_writing import get_tex_config
|
||||
from manimlib.utils.tex_file_writing import tex_to_svg_file
|
||||
from manimlib.utils.tex_file_writing import tex_content_to_svg_file
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@ -44,6 +43,8 @@ class SingleStringTex(SVGMobject):
|
||||
"alignment": "\\centering",
|
||||
"math_mode": True,
|
||||
"organize_left_to_right": False,
|
||||
"template": "",
|
||||
"additional_preamble": "",
|
||||
}
|
||||
|
||||
def __init__(self, tex_string: str, **kwargs):
|
||||
@ -64,27 +65,24 @@ class SingleStringTex(SVGMobject):
|
||||
self.path_string_config,
|
||||
self.tex_string,
|
||||
self.alignment,
|
||||
self.math_mode
|
||||
self.math_mode,
|
||||
self.template,
|
||||
self.additional_preamble
|
||||
)
|
||||
|
||||
def get_file_path(self) -> str:
|
||||
full_tex = self.get_tex_file_body(self.tex_string)
|
||||
content = self.get_tex_file_body(self.tex_string)
|
||||
with display_during_execution(f"Writing \"{self.tex_string}\""):
|
||||
file_path = tex_to_svg_file(full_tex)
|
||||
file_path = tex_content_to_svg_file(
|
||||
content, self.template, self.additional_preamble
|
||||
)
|
||||
return file_path
|
||||
|
||||
def get_tex_file_body(self, tex_string: str) -> str:
|
||||
new_tex = self.get_modified_expression(tex_string)
|
||||
if self.math_mode:
|
||||
new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
|
||||
|
||||
new_tex = self.alignment + "\n" + new_tex
|
||||
|
||||
tex_config = get_tex_config()
|
||||
return tex_config["tex_body"].replace(
|
||||
tex_config["text_to_replace"],
|
||||
new_tex
|
||||
)
|
||||
return self.alignment + "\n" + new_tex
|
||||
|
||||
def get_modified_expression(self, tex_string: str) -> str:
|
||||
return self.modify_special_strings(tex_string.strip())
|
||||
|
@ -18,7 +18,7 @@ from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.customization import get_customization
|
||||
from manimlib.utils.directories import get_downloads_dir
|
||||
from manimlib.utils.directories import get_text_dir
|
||||
from manimlib.utils.tex_file_writing import tex_hash
|
||||
from manimlib.utils.simple_functions import hash_string
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@ -63,7 +63,6 @@ class _Alignment:
|
||||
|
||||
class MarkupText(StringMobject):
|
||||
CONFIG = {
|
||||
"is_markup": True,
|
||||
"font_size": 48,
|
||||
"lsh": None,
|
||||
"justify": False,
|
||||
@ -81,21 +80,11 @@ class MarkupText(StringMobject):
|
||||
"t2w": {},
|
||||
"global_config": {},
|
||||
"local_configs": {},
|
||||
# For backward compatibility
|
||||
"isolate": (re.compile(r"[a-zA-Z]+"), re.compile(r"\S+")),
|
||||
"disable_ligatures": True,
|
||||
"isolate": re.compile(r"\w+", re.U),
|
||||
}
|
||||
|
||||
# See https://docs.gtk.org/Pango/pango_markup.html
|
||||
MARKUP_COLOR_KEYS = {
|
||||
"foreground": False,
|
||||
"fgcolor": False,
|
||||
"color": False,
|
||||
"background": True,
|
||||
"bgcolor": True,
|
||||
"underline_color": True,
|
||||
"overline_color": True,
|
||||
"strikethrough_color": True,
|
||||
}
|
||||
MARKUP_TAGS = {
|
||||
"b": {"font_weight": "bold"},
|
||||
"big": {"font_size": "larger"},
|
||||
@ -107,17 +96,24 @@ class MarkupText(StringMobject):
|
||||
"tt": {"font_family": "monospace"},
|
||||
"u": {"underline": "single"},
|
||||
}
|
||||
MARKUP_ENTITY_DICT = {
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"&": "&",
|
||||
"\"": """,
|
||||
"'": "'"
|
||||
}
|
||||
|
||||
def __init__(self, text: str, **kwargs):
|
||||
self.full2short(kwargs)
|
||||
digest_config(self, kwargs)
|
||||
|
||||
if not isinstance(self, Text):
|
||||
self.validate_markup_string(text)
|
||||
if not self.font:
|
||||
self.font = get_customization()["style"]["font"]
|
||||
if not self.alignment:
|
||||
self.alignment = get_customization()["style"]["text_alignment"]
|
||||
if self.is_markup:
|
||||
self.validate_markup_string(text)
|
||||
|
||||
self.text = text
|
||||
super().__init__(text, **kwargs)
|
||||
@ -140,8 +136,8 @@ class MarkupText(StringMobject):
|
||||
self.path_string_config,
|
||||
self.base_color,
|
||||
self.isolate,
|
||||
self.protect,
|
||||
self.text,
|
||||
self.is_markup,
|
||||
self.font_size,
|
||||
self.lsh,
|
||||
self.justify,
|
||||
@ -156,7 +152,8 @@ class MarkupText(StringMobject):
|
||||
self.t2s,
|
||||
self.t2w,
|
||||
self.global_config,
|
||||
self.local_configs
|
||||
self.local_configs,
|
||||
self.disable_ligatures
|
||||
)
|
||||
|
||||
def full2short(self, config: dict) -> None:
|
||||
@ -182,7 +179,7 @@ class MarkupText(StringMobject):
|
||||
self.line_width
|
||||
))
|
||||
svg_file = os.path.join(
|
||||
get_text_dir(), tex_hash(hash_content) + ".svg"
|
||||
get_text_dir(), hash_string(hash_content) + ".svg"
|
||||
)
|
||||
if not os.path.exists(svg_file):
|
||||
self.markup_to_svg(content, svg_file)
|
||||
@ -229,76 +226,92 @@ class MarkupText(StringMobject):
|
||||
f"{validate_error}"
|
||||
)
|
||||
|
||||
# Toolkits
|
||||
|
||||
@staticmethod
|
||||
def escape_markup_char(substr: str) -> str:
|
||||
return MarkupText.MARKUP_ENTITY_DICT.get(substr, substr)
|
||||
|
||||
@staticmethod
|
||||
def unescape_markup_char(substr: str) -> str:
|
||||
return {
|
||||
v: k
|
||||
for k, v in MarkupText.MARKUP_ENTITY_DICT.items()
|
||||
}.get(substr, substr)
|
||||
|
||||
# Parsing
|
||||
|
||||
def get_cmd_spans(self) -> list[Span]:
|
||||
if not self.is_markup:
|
||||
return self.find_spans(r"""[<>&"']""")
|
||||
@staticmethod
|
||||
def get_command_matches(string: str) -> list[re.Match]:
|
||||
pattern = re.compile(r"""
|
||||
(?P<tag>
|
||||
<
|
||||
(?P<close_slash>/)?
|
||||
(?P<tag_name>\w+)\s*
|
||||
(?P<attr_list>(?:\w+\s*\=\s*(?P<quot>["']).*?(?P=quot)\s*)*)
|
||||
(?P<elision_slash>/)?
|
||||
>
|
||||
)
|
||||
|(?P<passthrough>
|
||||
<\?.*?\?>|<!--.*?-->|<!\[CDATA\[.*?\]\]>|<!DOCTYPE.*?>
|
||||
)
|
||||
|(?P<entity>&(?P<unicode>\#(?P<hex>x)?)?(?P<content>.*?);)
|
||||
|(?P<char>[>"'])
|
||||
""", flags=re.X | re.S)
|
||||
return list(pattern.finditer(string))
|
||||
|
||||
# Unsupported passthroughs:
|
||||
# "<?...?>", "<!--...-->", "<![CDATA[...]]>", "<!DOCTYPE...>"
|
||||
# See https://gitlab.gnome.org/GNOME/glib/-/blob/main/glib/gmarkup.c
|
||||
return self.find_spans(
|
||||
r"""&[\s\S]*?;|[>"']|</?\w+(?:\s*\w+\s*\=\s*(["'])[\s\S]*?\1)*/?>"""
|
||||
)
|
||||
|
||||
def get_substr_flag(self, substr: str) -> int:
|
||||
if re.fullmatch(r"<\w[\s\S]*[^/]>", substr):
|
||||
return 1
|
||||
if substr.startswith("</"):
|
||||
return -1
|
||||
@staticmethod
|
||||
def get_command_flag(match_obj: re.Match) -> int:
|
||||
if match_obj.group("tag"):
|
||||
if match_obj.group("close_slash"):
|
||||
return -1
|
||||
if not match_obj.group("elision_slash"):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def get_repl_substr_for_content(self, substr: str) -> str:
|
||||
if substr.startswith("<") and substr.endswith(">"):
|
||||
@staticmethod
|
||||
def replace_for_content(match_obj: re.Match) -> str:
|
||||
if match_obj.group("tag"):
|
||||
return ""
|
||||
return {
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"&": "&",
|
||||
"\"": """,
|
||||
"'": "'"
|
||||
}.get(substr, substr)
|
||||
if match_obj.group("char"):
|
||||
return MarkupText.escape_markup_char(match_obj.group("char"))
|
||||
return match_obj.group()
|
||||
|
||||
def get_repl_substr_for_matching(self, substr: str) -> str:
|
||||
if substr.startswith("<") and substr.endswith(">"):
|
||||
@staticmethod
|
||||
def replace_for_matching(match_obj: re.Match) -> str:
|
||||
if match_obj.group("tag") or match_obj.group("passthrough"):
|
||||
return ""
|
||||
if substr.startswith("&#") and substr.endswith(";"):
|
||||
if substr.startswith("&#x"):
|
||||
char_reference = int(substr[3:-1], 16)
|
||||
else:
|
||||
char_reference = int(substr[2:-1], 10)
|
||||
return chr(char_reference)
|
||||
return {
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"&": "&",
|
||||
""": "\"",
|
||||
"'": "'"
|
||||
}.get(substr, substr)
|
||||
if match_obj.group("entity"):
|
||||
if match_obj.group("unicode"):
|
||||
base = 10
|
||||
if match_obj.group("hex"):
|
||||
base = 16
|
||||
return chr(int(match_obj.group("content"), base))
|
||||
return MarkupText.unescape_markup_char(match_obj.group("entity"))
|
||||
return match_obj.group()
|
||||
|
||||
def get_specified_items(
|
||||
self, cmd_span_pairs: list[tuple[Span, Span]]
|
||||
) -> list[tuple[Span, dict[str, str]]]:
|
||||
attr_pattern = r"""(\w+)\s*\=\s*(["'])([\s\S]*?)\2"""
|
||||
internal_items = []
|
||||
for begin_cmd_span, end_cmd_span in cmd_span_pairs:
|
||||
begin_tag = self.get_substr(begin_cmd_span)
|
||||
tag_name = re.match(r"<(\w+)", begin_tag).group(1)
|
||||
if tag_name == "span":
|
||||
attr_dict = {
|
||||
attr_match_obj.group(1): attr_match_obj.group(3)
|
||||
for attr_match_obj in re.finditer(attr_pattern, begin_tag)
|
||||
}
|
||||
else:
|
||||
attr_dict = MarkupText.MARKUP_TAGS.get(tag_name, {})
|
||||
internal_items.append(
|
||||
((begin_cmd_span[1], end_cmd_span[0]), attr_dict)
|
||||
)
|
||||
@staticmethod
|
||||
def get_attr_dict_from_command_pair(
|
||||
open_command: re.Match, close_command: re.Match
|
||||
) -> dict[str, str] | None:
|
||||
pattern = r"""
|
||||
(?P<attr_name>\w+)
|
||||
\s*\=\s*
|
||||
(?P<quot>["'])(?P<attr_val>.*?)(?P=quot)
|
||||
"""
|
||||
tag_name = open_command.group("tag_name")
|
||||
if tag_name == "span":
|
||||
return {
|
||||
match_obj.group("attr_name"): match_obj.group("attr_val")
|
||||
for match_obj in re.finditer(
|
||||
pattern, open_command.group("attr_list"), re.S | re.X
|
||||
)
|
||||
}
|
||||
return MarkupText.MARKUP_TAGS.get(tag_name, {})
|
||||
|
||||
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
|
||||
return [
|
||||
*internal_items,
|
||||
*[
|
||||
*(
|
||||
(span, {key: val})
|
||||
for t2x_dict, key in (
|
||||
(self.t2c, "foreground"),
|
||||
@ -308,49 +321,49 @@ class MarkupText(StringMobject):
|
||||
)
|
||||
for selector, val in t2x_dict.items()
|
||||
for span in self.find_spans_by_selector(selector)
|
||||
],
|
||||
*[
|
||||
),
|
||||
*(
|
||||
(span, local_config)
|
||||
for selector, local_config in self.local_configs.items()
|
||||
for span in self.find_spans_by_selector(selector)
|
||||
],
|
||||
*[
|
||||
(span, {})
|
||||
for span in self.find_spans_by_selector(self.isolate)
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_cmd_str_pair(
|
||||
attr_dict: dict[str, str], label_hex: str | None
|
||||
) -> tuple[str, str]:
|
||||
def get_command_string(
|
||||
attr_dict: dict[str, str], is_end: bool, label_hex: str | None
|
||||
) -> str:
|
||||
if is_end:
|
||||
return "</span>"
|
||||
|
||||
if label_hex is not None:
|
||||
converted_attr_dict = {"foreground": label_hex}
|
||||
for key, val in attr_dict.items():
|
||||
substitute_key = MarkupText.MARKUP_COLOR_KEYS.get(key, None)
|
||||
if substitute_key is None:
|
||||
converted_attr_dict[key] = val
|
||||
elif substitute_key:
|
||||
if key in (
|
||||
"background", "bgcolor",
|
||||
"underline_color", "overline_color", "strikethrough_color"
|
||||
):
|
||||
converted_attr_dict[key] = "black"
|
||||
elif key not in ("foreground", "fgcolor", "color"):
|
||||
converted_attr_dict[key] = val
|
||||
else:
|
||||
converted_attr_dict = attr_dict.copy()
|
||||
attrs_str = " ".join([
|
||||
f"{key}='{val}'"
|
||||
for key, val in converted_attr_dict.items()
|
||||
])
|
||||
return f"<span {attrs_str}>", "</span>"
|
||||
return f"<span {attrs_str}>"
|
||||
|
||||
def get_content_prefix_and_suffix(
|
||||
self, is_labelled: bool
|
||||
) -> tuple[str, str]:
|
||||
global_attr_dict = {
|
||||
"foreground": self.base_color_hex,
|
||||
"foreground": self.color_to_hex(self.base_color),
|
||||
"font_family": self.font,
|
||||
"font_style": self.slant,
|
||||
"font_weight": self.weight,
|
||||
"font_size": str(self.font_size * 1024),
|
||||
"font_size": str(round(self.font_size * 1024)),
|
||||
}
|
||||
global_attr_dict.update(self.global_config)
|
||||
# `line_height` attribute is supported since Pango 1.50.
|
||||
pango_version = manimpango.pango_version()
|
||||
if tuple(map(int, pango_version.split("."))) < (1, 50):
|
||||
@ -365,10 +378,17 @@ class MarkupText(StringMobject):
|
||||
global_attr_dict["line_height"] = str(
|
||||
((line_spacing_scale) + 1) * 0.6
|
||||
)
|
||||
if self.disable_ligatures:
|
||||
global_attr_dict["font_features"] = "liga=0,dlig=0,clig=0,hlig=0"
|
||||
|
||||
return self.get_cmd_str_pair(
|
||||
global_attr_dict,
|
||||
label_hex=self.int_to_hex(0) if is_labelled else None
|
||||
global_attr_dict.update(self.global_config)
|
||||
return tuple(
|
||||
self.get_command_string(
|
||||
global_attr_dict,
|
||||
is_end=is_end,
|
||||
label_hex=self.int_to_hex(0) if is_labelled else None
|
||||
)
|
||||
for is_end in (False, True)
|
||||
)
|
||||
|
||||
# Method alias
|
||||
@ -376,8 +396,8 @@ class MarkupText(StringMobject):
|
||||
def get_parts_by_text(self, selector: Selector) -> VGroup:
|
||||
return self.select_parts(selector)
|
||||
|
||||
def get_part_by_text(self, selector: Selector) -> VGroup:
|
||||
return self.select_part(selector)
|
||||
def get_part_by_text(self, selector: Selector, **kwargs) -> VGroup:
|
||||
return self.select_part(selector, **kwargs)
|
||||
|
||||
def set_color_by_text(self, selector: Selector, color: ManimColor):
|
||||
return self.set_parts_color(selector, color)
|
||||
@ -393,9 +413,27 @@ class MarkupText(StringMobject):
|
||||
|
||||
class Text(MarkupText):
|
||||
CONFIG = {
|
||||
"is_markup": False,
|
||||
# For backward compatibility
|
||||
"isolate": (re.compile(r"\w+", re.U), re.compile(r"\S+", re.U)),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_command_matches(string: str) -> list[re.Match]:
|
||||
pattern = re.compile(r"""[<>&"']""")
|
||||
return list(pattern.finditer(string))
|
||||
|
||||
@staticmethod
|
||||
def get_command_flag(match_obj: re.Match) -> int:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def replace_for_content(match_obj: re.Match) -> str:
|
||||
return Text.escape_markup_char(match_obj.group())
|
||||
|
||||
@staticmethod
|
||||
def replace_for_matching(match_obj: re.Match) -> str:
|
||||
return match_obj.group()
|
||||
|
||||
|
||||
class Code(MarkupText):
|
||||
CONFIG = {
|
||||
|
732
manimlib/tex_templates.yml
Normal file
732
manimlib/tex_templates.yml
Normal file
@ -0,0 +1,732 @@
|
||||
# Classical TeX templates
|
||||
|
||||
default:
|
||||
description: ""
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage[english]{babel}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{dsfont}
|
||||
\usepackage{setspace}
|
||||
\usepackage{tipa}
|
||||
\usepackage{relsize}
|
||||
\usepackage{textcomp}
|
||||
\usepackage{mathrsfs}
|
||||
\usepackage{calligra}
|
||||
\usepackage{wasysym}
|
||||
\usepackage{ragged2e}
|
||||
\usepackage{physics}
|
||||
\usepackage{xcolor}
|
||||
\usepackage{microtype}
|
||||
\usepackage{pifont}
|
||||
\DisableLigatures{encoding = *, family = * }
|
||||
\linespread{1}
|
||||
|
||||
ctex:
|
||||
description: ""
|
||||
compiler: xelatex
|
||||
preamble: |-
|
||||
\usepackage[UTF8]{ctex}
|
||||
\usepackage[english]{babel}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{dsfont}
|
||||
\usepackage{setspace}
|
||||
\usepackage{tipa}
|
||||
\usepackage{relsize}
|
||||
\usepackage{textcomp}
|
||||
\usepackage{mathrsfs}
|
||||
\usepackage{calligra}
|
||||
\usepackage{wasysym}
|
||||
\usepackage{ragged2e}
|
||||
\usepackage{physics}
|
||||
\usepackage{xcolor}
|
||||
\usepackage{microtype}
|
||||
\linespread{1}
|
||||
|
||||
# Simplified TeX templates
|
||||
|
||||
basic:
|
||||
description: ""
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage[english]{babel}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
|
||||
basic_ctex:
|
||||
description: ""
|
||||
compiler: xelatex
|
||||
preamble: |-
|
||||
\usepackage[UTF8]{ctex}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
|
||||
empty:
|
||||
description: ""
|
||||
compiler: latex
|
||||
preamble: ""
|
||||
|
||||
empty_ctex:
|
||||
description: ""
|
||||
compiler: xelatex
|
||||
preamble: ""
|
||||
|
||||
# A collection of TeX templates for the fonts described at
|
||||
# http://jf.burnol.free.fr/showcase.html
|
||||
|
||||
american_typewriter:
|
||||
description: American Typewriter
|
||||
compiler: xelatex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[no-math]{fontspec}
|
||||
\setmainfont[Mapping=tex-text]{American Typewriter}
|
||||
\usepackage[defaultmathsizes]{mathastext}
|
||||
|
||||
antykwa:
|
||||
description: Antykwa Poltawskiego (TX Fonts for Greek and math symbols)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[OT4,OT1]{fontenc}
|
||||
\usepackage{txfonts}
|
||||
\usepackage[upright]{txgreeks}
|
||||
\usepackage{antpolt}
|
||||
\usepackage[defaultmathsizes,nolessnomore]{mathastext}
|
||||
|
||||
apple_chancery:
|
||||
description: Apple Chancery
|
||||
compiler: xelatex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[no-math]{fontspec}
|
||||
\setmainfont[Mapping=tex-text]{Apple Chancery}
|
||||
\usepackage[defaultmathsizes]{mathastext}
|
||||
|
||||
auriocus_kalligraphicus:
|
||||
description: Auriocus Kalligraphicus (Symbol Greek)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{aurical}
|
||||
\renewcommand{\rmdefault}{AuriocusKalligraphicus}
|
||||
\usepackage[symbolgreek]{mathastext}
|
||||
|
||||
baskervald_adf_fourier:
|
||||
description: Baskervald ADF with Fourier
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[upright]{fourier}
|
||||
\usepackage{baskervald}
|
||||
\usepackage[defaultmathsizes,noasterisk]{mathastext}
|
||||
|
||||
baskerville_it:
|
||||
description: Baskerville (Italic)
|
||||
compiler: xelatex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[no-math]{fontspec}
|
||||
\setmainfont[Mapping=tex-text]{Baskerville}
|
||||
\usepackage[defaultmathsizes,italic]{mathastext}
|
||||
|
||||
biolinum:
|
||||
description: Biolinum
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage{txfonts}
|
||||
\usepackage[upright]{txgreeks}
|
||||
\usepackage[no-math]{fontspec}
|
||||
\setmainfont[Mapping=tex-text]{Minion Pro}
|
||||
\setsansfont[Mapping=tex-text,Scale=MatchUppercase]{Myriad Pro}
|
||||
\renewcommand\familydefault\sfdefault
|
||||
\usepackage[defaultmathsizes]{mathastext}
|
||||
\renewcommand\familydefault\rmdefault
|
||||
|
||||
brushscriptx:
|
||||
description: BrushScriptX-Italic (PX math and Greek)
|
||||
compiler: xelatex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{pxfonts}
|
||||
\renewcommand{\rmdefault}{pbsi}
|
||||
\renewcommand{\mddefault}{xl}
|
||||
\renewcommand{\bfdefault}{xl}
|
||||
\usepackage[defaultmathsizes,noasterisk]{mathastext}
|
||||
\boldmath
|
||||
|
||||
chalkboard_se:
|
||||
description: Chalkboard SE
|
||||
compiler: xelatex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[no-math]{fontspec}
|
||||
\setmainfont[Mapping=tex-text]{Chalkboard SE}
|
||||
\usepackage[defaultmathsizes]{mathastext}
|
||||
|
||||
chalkduster:
|
||||
description: Chalkduster
|
||||
compiler: lualatex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[no-math]{fontspec}
|
||||
\setmainfont[Mapping=tex-text]{Chalkduster}
|
||||
\usepackage[defaultmathsizes]{mathastext}
|
||||
|
||||
comfortaa:
|
||||
description: Comfortaa
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[default]{comfortaa}
|
||||
\usepackage[LGRgreek,defaultmathsizes,noasterisk]{mathastext}
|
||||
\let\varphi\phi
|
||||
\linespread{1.06}
|
||||
|
||||
comic_sans:
|
||||
description: Comic Sans MS
|
||||
compiler: xelatex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[no-math]{fontspec}
|
||||
\setmainfont[Mapping=tex-text]{Comic Sans MS}
|
||||
\usepackage[defaultmathsizes]{mathastext}
|
||||
|
||||
droid_sans:
|
||||
description: Droid Sans
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[default]{droidsans}
|
||||
\usepackage[LGRgreek]{mathastext}
|
||||
\let\varepsilon\epsilon
|
||||
|
||||
droid_sans_it:
|
||||
description: Droid Sans (Italic)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[default]{droidsans}
|
||||
\usepackage[LGRgreek,defaultmathsizes,italic]{mathastext}
|
||||
\let\varphi\phi
|
||||
|
||||
droid_serif:
|
||||
description: Droid Serif
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[default]{droidserif}
|
||||
\usepackage[LGRgreek]{mathastext}
|
||||
\let\varepsilon\epsilon
|
||||
|
||||
droid_serif_px_it:
|
||||
description: Droid Serif (PX math symbols) (Italic)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{pxfonts}
|
||||
\usepackage[default]{droidserif}
|
||||
\usepackage[LGRgreek,defaultmathsizes,italic,basic]{mathastext}
|
||||
\let\varphi\phi
|
||||
|
||||
ecf_augie:
|
||||
description: ECF Augie (Euler Greek)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\renewcommand\familydefault{fau}
|
||||
\usepackage[defaultmathsizes,eulergreek]{mathastext}
|
||||
|
||||
ecf_jd:
|
||||
description: ECF JD (with TX fonts)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage{txfonts}
|
||||
\usepackage[upright]{txgreeks}
|
||||
\renewcommand\familydefault{fjd}
|
||||
\usepackage{mathastext}
|
||||
\mathversion{bold}
|
||||
|
||||
ecf_skeetch:
|
||||
description: ECF Skeetch (CM Greek)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\DeclareFontFamily{T1}{fsk}{}
|
||||
\DeclareFontShape{T1}{fsk}{m}{n}{<->s*[1.315] fskmw8t}{}
|
||||
\renewcommand\rmdefault{fsk}
|
||||
\usepackage[noendash,defaultmathsizes,nohbar,defaultimath]{mathastext}
|
||||
|
||||
ecf_tall_paul:
|
||||
description: ECF Tall Paul (with Symbol font)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\DeclareFontFamily{T1}{ftp}{}
|
||||
\DeclareFontShape{T1}{ftp}{m}{n}{<->s*[1.4] ftpmw8t}{}
|
||||
\renewcommand\familydefault{ftp}
|
||||
\usepackage[symbol]{mathastext}
|
||||
\let\infty\inftypsy
|
||||
|
||||
ecf_webster:
|
||||
description: ECF Webster (with TX fonts)
|
||||
compiler: xelatex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage{txfonts}
|
||||
\usepackage[upright]{txgreeks}
|
||||
\renewcommand\familydefault{fwb}
|
||||
\usepackage{mathastext}
|
||||
\renewcommand{\int}{\intop\limits}
|
||||
\linespread{1.5}
|
||||
\mathversion{bold}
|
||||
|
||||
electrum_adf:
|
||||
description: Electrum ADF (CM Greek)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[LGRgreek,basic,defaultmathsizes]{mathastext}
|
||||
\usepackage[lf]{electrum}
|
||||
\Mathastext
|
||||
\let\varphi\phi
|
||||
|
||||
epigrafica:
|
||||
description: Epigrafica
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[LGR,OT1]{fontenc}
|
||||
\usepackage{epigrafica}
|
||||
\usepackage[basic,LGRgreek,defaultmathsizes]{mathastext}
|
||||
\let\varphi\phi
|
||||
\linespread{1.2}
|
||||
|
||||
fourier_utopia:
|
||||
description: Fourier Utopia (Fourier upright Greek)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[upright]{fourier}
|
||||
\usepackage{mathastext}
|
||||
|
||||
french_cursive:
|
||||
description: French Cursive (Euler Greek)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[default]{frcursive}
|
||||
\usepackage[eulergreek,noplusnominus,noequal,nohbar,nolessnomore,noasterisk]{mathastext}
|
||||
|
||||
gfs_bodoni:
|
||||
description: GFS Bodoni
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\renewcommand{\rmdefault}{bodoni}
|
||||
\usepackage[LGRgreek]{mathastext}
|
||||
\let\varphi\phi
|
||||
\linespread{1.06}
|
||||
|
||||
gfs_didot:
|
||||
description: GFS Didot (Italic)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\renewcommand\rmdefault{udidot}
|
||||
\usepackage[LGRgreek,defaultmathsizes,italic]{mathastext}
|
||||
\let\varphi\phi
|
||||
|
||||
gfs_neohellenic:
|
||||
description: GFS NeoHellenic
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\renewcommand{\rmdefault}{neohellenic}
|
||||
\usepackage[LGRgreek]{mathastext}
|
||||
\let\varphi\phi
|
||||
\linespread{1.06}
|
||||
|
||||
gnu_freesans_tx:
|
||||
description: GNU FreeSerif (and TX fonts symbols)
|
||||
compiler: xelatex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[no-math]{fontspec}
|
||||
\usepackage{txfonts}
|
||||
\setmainfont[ExternalLocation,Mapping=tex-text,BoldFont=FreeSerifBold,ItalicFont=FreeSerifItalic,BoldItalicFont=FreeSerifBoldItalic]{FreeSerif}
|
||||
\usepackage[defaultmathsizes]{mathastext}
|
||||
|
||||
gnu_freeserif_freesans:
|
||||
description: GNU FreeSerif and FreeSans
|
||||
compiler: xelatex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[no-math]{fontspec}
|
||||
\setmainfont[ExternalLocation,Mapping=tex-text,BoldFont=FreeSerifBold,ItalicFont=FreeSerifItalic,BoldItalicFont=FreeSerifBoldItalic]{FreeSerif}
|
||||
\setsansfont[ExternalLocation,Mapping=tex-text,BoldFont=FreeSansBold,ItalicFont=FreeSansOblique,BoldItalicFont=FreeSansBoldOblique,Scale=MatchLowercase]{FreeSans}
|
||||
\renewcommand{\familydefault}{lmss}
|
||||
\usepackage[LGRgreek,defaultmathsizes,noasterisk]{mathastext}
|
||||
\renewcommand{\familydefault}{\sfdefault}
|
||||
\Mathastext
|
||||
\let\varphi\phi
|
||||
\renewcommand{\familydefault}{\rmdefault}
|
||||
|
||||
helvetica_fourier_it:
|
||||
description: Helvetica with Fourier (Italic)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[scaled]{helvet}
|
||||
\usepackage{fourier}
|
||||
\renewcommand{\rmdefault}{phv}
|
||||
\usepackage[italic,defaultmathsizes,noasterisk]{mathastext}
|
||||
|
||||
latin_modern_tw:
|
||||
description: Latin Modern Typewriter Proportional
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[variablett]{lmodern}
|
||||
\renewcommand{\rmdefault}{\ttdefault}
|
||||
\usepackage[LGRgreek]{mathastext}
|
||||
\MTgreekfont{lmtt}
|
||||
\Mathastext
|
||||
\let\varepsilon\epsilon
|
||||
|
||||
latin_modern_tw_it:
|
||||
description: Latin Modern Typewriter Proportional (CM Greek) (Italic)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[variablett,nomath]{lmodern}
|
||||
\renewcommand{\familydefault}{\ttdefault}
|
||||
\usepackage[frenchmath]{mathastext}
|
||||
\linespread{1.08}
|
||||
|
||||
libertine:
|
||||
description: Libertine
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{libertine}
|
||||
\usepackage[greek=n]{libgreek}
|
||||
\usepackage[noasterisk,defaultmathsizes]{mathastext}
|
||||
|
||||
libris_adf_fourier:
|
||||
description: Libris ADF with Fourier
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[upright]{fourier}
|
||||
\usepackage{libris}
|
||||
\renewcommand{\familydefault}{\sfdefault}
|
||||
\usepackage[noasterisk]{mathastext}
|
||||
|
||||
minion_pro_myriad_pro:
|
||||
description: Minion Pro and Myriad Pro (and TX fonts symbols)
|
||||
compiler: xelatex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[default]{droidserif}
|
||||
\usepackage[LGRgreek]{mathastext}
|
||||
\let\varepsilon\epsilon
|
||||
|
||||
minion_pro_tx:
|
||||
description: Minion Pro (and TX fonts symbols)
|
||||
compiler: xelatex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage{txfonts}
|
||||
\usepackage[no-math]{fontspec}
|
||||
\setmainfont[Mapping=tex-text]{Minion Pro}
|
||||
\usepackage[defaultmathsizes]{mathastext}
|
||||
|
||||
new_century_schoolbook:
|
||||
description: New Century Schoolbook (Symbol Greek)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{newcent}
|
||||
\usepackage[symbolgreek]{mathastext}
|
||||
\linespread{1.1}
|
||||
|
||||
new_century_schoolbook_px:
|
||||
description: New Century Schoolbook (Symbol Greek, PX math symbols)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{pxfonts}
|
||||
\usepackage{newcent}
|
||||
\usepackage[symbolgreek,defaultmathsizes]{mathastext}
|
||||
\linespread{1.06}
|
||||
|
||||
noteworthy_light:
|
||||
description: Noteworthy Light
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[no-math]{fontspec}
|
||||
\setmainfont[Mapping=tex-text]{Noteworthy Light}
|
||||
\usepackage[defaultmathsizes]{mathastext}
|
||||
|
||||
palatino:
|
||||
description: Palatino (Symbol Greek)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{palatino}
|
||||
\usepackage[symbolmax,defaultmathsizes]{mathastext}
|
||||
|
||||
papyrus:
|
||||
description: Papyrus
|
||||
compiler: xelatex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[no-math]{fontspec}
|
||||
\setmainfont[Mapping=tex-text]{Papyrus}
|
||||
\usepackage[defaultmathsizes]{mathastext}
|
||||
|
||||
romande_adf_fourier_it:
|
||||
description: Romande ADF with Fourier (Italic)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{fourier}
|
||||
\usepackage{romande}
|
||||
\usepackage[italic,defaultmathsizes,noasterisk]{mathastext}
|
||||
\renewcommand{\itshape}{\swashstyle}
|
||||
|
||||
slitex:
|
||||
description: SliTeX (Euler Greek)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{tpslifonts}
|
||||
\usepackage[eulergreek,defaultmathsizes]{mathastext}
|
||||
\MTEulerScale{1.06}
|
||||
\linespread{1.2}
|
||||
|
||||
times_fourier_it:
|
||||
description: Times with Fourier (Italic)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage{fourier}
|
||||
\renewcommand{\rmdefault}{ptm}
|
||||
\usepackage[italic,defaultmathsizes,noasterisk]{mathastext}
|
||||
|
||||
urw_avant_garde:
|
||||
description: URW Avant Garde (Symbol Greek)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{avant}
|
||||
\renewcommand{\familydefault}{\sfdefault}
|
||||
\usepackage[symbolgreek,defaultmathsizes]{mathastext}
|
||||
|
||||
urw_zapf_chancery:
|
||||
description: URW Zapf Chancery (CM Greek)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\DeclareFontFamily{T1}{pzc}{}
|
||||
\DeclareFontShape{T1}{pzc}{mb}{it}{<->s*[1.2] pzcmi8t}{}
|
||||
\DeclareFontShape{T1}{pzc}{m}{it}{<->ssub * pzc/mb/it}{}
|
||||
\DeclareFontShape{T1}{pzc}{mb}{sl}{<->ssub * pzc/mb/it}{}
|
||||
\DeclareFontShape{T1}{pzc}{m}{sl}{<->ssub * pzc/mb/sl}{}
|
||||
\DeclareFontShape{T1}{pzc}{m}{n}{<->ssub * pzc/mb/it}{}
|
||||
\usepackage{chancery}
|
||||
\usepackage{mathastext}
|
||||
\linespread{1.05}
|
||||
\boldmath
|
||||
|
||||
venturis_adf_fourier_it:
|
||||
description: Venturis ADF with Fourier (Italic)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage{fourier}
|
||||
\usepackage[lf]{venturis}
|
||||
\usepackage[italic,defaultmathsizes,noasterisk]{mathastext}
|
||||
|
||||
verdana_it:
|
||||
description: Verdana (Italic)
|
||||
compiler: xelatex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[no-math]{fontspec}
|
||||
\setmainfont[Mapping=tex-text]{Verdana}
|
||||
\usepackage[defaultmathsizes,italic]{mathastext}
|
||||
|
||||
vollkorn:
|
||||
description: Vollkorn (TX fonts for Greek and math symbols)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{txfonts}
|
||||
\usepackage[upright]{txgreeks}
|
||||
\usepackage{vollkorn}
|
||||
\usepackage[defaultmathsizes]{mathastext}
|
||||
|
||||
vollkorn_fourier_it:
|
||||
description: Vollkorn with Fourier (Italic)
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\usepackage{fourier}
|
||||
\usepackage{vollkorn}
|
||||
\usepackage[italic,nohbar]{mathastext}
|
||||
|
||||
zapf_chancery:
|
||||
description: Zapf Chancery
|
||||
compiler: latex
|
||||
preamble: |-
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{xcolor}
|
||||
\DeclareFontFamily{T1}{pzc}{}
|
||||
\DeclareFontShape{T1}{pzc}{mb}{it}{<->s*[1.2] pzcmi8t}{}
|
||||
\DeclareFontShape{T1}{pzc}{m}{it}{<->ssub * pzc/mb/it}{}
|
||||
\usepackage{chancery}
|
||||
\renewcommand\shapedefault\itdefault
|
||||
\renewcommand\bfdefault\mddefault
|
||||
\usepackage[defaultmathsizes]{mathastext}
|
||||
\linespread{1.05}
|
@ -1,25 +0,0 @@
|
||||
\documentclass[preview]{standalone}
|
||||
\usepackage[UTF8]{ctex}
|
||||
|
||||
\usepackage[english]{babel}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{dsfont}
|
||||
\usepackage{setspace}
|
||||
\usepackage{tipa}
|
||||
\usepackage{relsize}
|
||||
\usepackage{textcomp}
|
||||
\usepackage{mathrsfs}
|
||||
\usepackage{calligra}
|
||||
\usepackage{wasysym}
|
||||
\usepackage{ragged2e}
|
||||
\usepackage{physics}
|
||||
\usepackage{xcolor}
|
||||
\usepackage{microtype}
|
||||
\linespread{1}
|
||||
|
||||
\begin{document}
|
||||
|
||||
[tex_expression]
|
||||
|
||||
\end{document}
|
@ -1,28 +0,0 @@
|
||||
\documentclass[preview]{standalone}
|
||||
|
||||
\usepackage[english]{babel}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{dsfont}
|
||||
\usepackage{setspace}
|
||||
\usepackage{tipa}
|
||||
\usepackage{relsize}
|
||||
\usepackage{textcomp}
|
||||
\usepackage{mathrsfs}
|
||||
\usepackage{calligra}
|
||||
\usepackage{wasysym}
|
||||
\usepackage{ragged2e}
|
||||
\usepackage{physics}
|
||||
\usepackage{xcolor}
|
||||
\usepackage{microtype}
|
||||
\usepackage{pifont}
|
||||
\DisableLigatures{encoding = *, family = * }
|
||||
\linespread{1}
|
||||
|
||||
\begin{document}
|
||||
|
||||
[tex_expression]
|
||||
|
||||
\end{document}
|
@ -42,14 +42,9 @@ def init_customization() -> None:
|
||||
"sounds": "",
|
||||
"temporary_storage": "",
|
||||
},
|
||||
"tex": {
|
||||
"executable": "",
|
||||
"template_file": "",
|
||||
"intermediate_filetype": "",
|
||||
"text_to_replace": "[tex_expression]",
|
||||
},
|
||||
"universal_import_line": "from manimlib import *",
|
||||
"style": {
|
||||
"tex_template": "",
|
||||
"font": "Consolas",
|
||||
"background_color": "",
|
||||
},
|
||||
@ -62,7 +57,7 @@ def init_customization() -> None:
|
||||
"medium": "1280x720",
|
||||
"high": "1920x1080",
|
||||
"4k": "3840x2160",
|
||||
"default_resolution": "high",
|
||||
"default_resolution": "",
|
||||
},
|
||||
"fps": 30,
|
||||
}
|
||||
@ -109,24 +104,14 @@ def init_customization() -> None:
|
||||
show_default=False
|
||||
)
|
||||
|
||||
console.print("[bold]LaTeX:[/bold]")
|
||||
tex_config = configuration["tex"]
|
||||
tex = Prompt.ask(
|
||||
" Select an executable program to use to compile a LaTeX source file",
|
||||
choices=["latex", "xelatex"],
|
||||
default="latex"
|
||||
)
|
||||
if tex == "latex":
|
||||
tex_config["executable"] = "latex"
|
||||
tex_config["template_file"] = "tex_template.tex"
|
||||
tex_config["intermediate_filetype"] = "dvi"
|
||||
else:
|
||||
tex_config["executable"] = "xelatex -no-pdf"
|
||||
tex_config["template_file"] = "ctex_template.tex"
|
||||
tex_config["intermediate_filetype"] = "xdv"
|
||||
|
||||
console.print("[bold]Styles:[/bold]")
|
||||
configuration["style"]["background_color"] = Prompt.ask(
|
||||
style_config = configuration["style"]
|
||||
tex_template = Prompt.ask(
|
||||
" Select a TeX template to compile a LaTeX source file",
|
||||
default="default"
|
||||
)
|
||||
style_config["tex_template"] = tex_template
|
||||
style_config["background_color"] = Prompt.ask(
|
||||
" Which [bold]background color[/bold] do you want [italic](hex code)",
|
||||
default="#333333"
|
||||
)
|
||||
@ -139,7 +124,7 @@ def init_customization() -> None:
|
||||
)
|
||||
table.add_row("480p15", "720p30", "1080p60", "2160p60")
|
||||
console.print(table)
|
||||
configuration["camera_qualities"]["default_quality"] = Prompt.ask(
|
||||
configuration["camera_resolutions"]["default_resolution"] = Prompt.ask(
|
||||
" Which one to choose as the default rendering quality",
|
||||
choices=["low", "medium", "high", "ultra_high"],
|
||||
default="high"
|
||||
@ -161,7 +146,7 @@ def init_customization() -> None:
|
||||
file_name = os.path.join(os.getcwd(), "custom_config.yml")
|
||||
with open(file_name, "w", encoding="utf-8") as f:
|
||||
yaml.dump(configuration, f)
|
||||
|
||||
|
||||
console.print(f"\n:rocket: You have successfully set up a {scope} configuration file!")
|
||||
console.print(f"You can manually modify it in: [cyan]`{file_name}`[/cyan]")
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
from functools import lru_cache
|
||||
import hashlib
|
||||
import inspect
|
||||
import math
|
||||
|
||||
@ -76,3 +77,9 @@ def binary_search(function,
|
||||
else:
|
||||
return None
|
||||
return mh
|
||||
|
||||
|
||||
def hash_string(string):
|
||||
# Truncating at 16 bytes for cleanliness
|
||||
hasher = hashlib.sha256(string.encode())
|
||||
return hasher.hexdigest()[:16]
|
||||
|
@ -1,135 +1,152 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import yaml
|
||||
|
||||
from manimlib.config import get_custom_config
|
||||
from manimlib.config import get_manim_dir
|
||||
from manimlib.logger import log
|
||||
from manimlib.utils.directories import get_tex_dir
|
||||
from manimlib.utils.simple_functions import hash_string
|
||||
|
||||
|
||||
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:
|
||||
templates_dict = yaml.safe_load(tex_templates_file)
|
||||
if name not in templates_dict:
|
||||
log.warning(
|
||||
"Cannot recognize template '%s', falling back to 'default'.",
|
||||
name
|
||||
)
|
||||
name = "default"
|
||||
return templates_dict[name]
|
||||
|
||||
|
||||
def get_tex_config() -> dict[str, str]:
|
||||
"""
|
||||
Returns a dict which should look something like this:
|
||||
{
|
||||
"executable": "latex",
|
||||
"template_file": "tex_template.tex",
|
||||
"intermediate_filetype": "dvi",
|
||||
"text_to_replace": "YourTextHere",
|
||||
"tex_body": "..."
|
||||
"template": "default",
|
||||
"compiler": "latex",
|
||||
"preamble": "..."
|
||||
}
|
||||
"""
|
||||
# Only load once, then save thereafter
|
||||
if not SAVED_TEX_CONFIG:
|
||||
custom_config = get_custom_config()
|
||||
SAVED_TEX_CONFIG.update(custom_config["tex"])
|
||||
# Read in template file
|
||||
template_filename = os.path.join(
|
||||
get_manim_dir(), "manimlib", "tex_templates",
|
||||
SAVED_TEX_CONFIG["template_file"],
|
||||
)
|
||||
with open(template_filename, "r", encoding="utf-8") as file:
|
||||
SAVED_TEX_CONFIG["tex_body"] = file.read()
|
||||
template_name = get_custom_config()["style"]["tex_template"]
|
||||
template_config = get_tex_template_config(template_name)
|
||||
SAVED_TEX_CONFIG.update({
|
||||
"template": template_name,
|
||||
"compiler": template_config["compiler"],
|
||||
"preamble": template_config["preamble"]
|
||||
})
|
||||
return SAVED_TEX_CONFIG
|
||||
|
||||
|
||||
def tex_hash(tex_file_content: str) -> int:
|
||||
# Truncating at 16 bytes for cleanliness
|
||||
hasher = hashlib.sha256(tex_file_content.encode())
|
||||
return hasher.hexdigest()[:16]
|
||||
def tex_content_to_svg_file(
|
||||
content: str, template: str, additional_preamble: 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}",
|
||||
preamble,
|
||||
"\\begin{document}",
|
||||
content,
|
||||
"\\end{document}"
|
||||
)) + "\n"
|
||||
|
||||
def tex_to_svg_file(tex_file_content: str) -> str:
|
||||
svg_file = os.path.join(
|
||||
get_tex_dir(), tex_hash(tex_file_content) + ".svg"
|
||||
get_tex_dir(), hash_string(full_tex) + ".svg"
|
||||
)
|
||||
if not os.path.exists(svg_file):
|
||||
# If svg doesn't exist, create it
|
||||
tex_to_svg(tex_file_content, svg_file)
|
||||
create_tex_svg(full_tex, svg_file, compiler)
|
||||
return svg_file
|
||||
|
||||
|
||||
def tex_to_svg(tex_file_content: str, svg_file: str) -> str:
|
||||
tex_file = svg_file.replace(".svg", ".tex")
|
||||
with open(tex_file, "w", encoding="utf-8") as outfile:
|
||||
outfile.write(tex_file_content)
|
||||
svg_file = dvi_to_svg(tex_to_dvi(tex_file))
|
||||
def create_tex_svg(full_tex: str, svg_file: str, compiler: str) -> None:
|
||||
if compiler == "latex":
|
||||
program = "latex"
|
||||
dvi_ext = ".dvi"
|
||||
elif compiler == "xelatex":
|
||||
program = "xelatex -no-pdf"
|
||||
dvi_ext = ".xdv"
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Compiler '{compiler}' is not implemented"
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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."
|
||||
)
|
||||
with open(root + ".log", "r", encoding="utf-8") as log_file:
|
||||
error_match_obj = re.search(r"(?<=\n! ).*", log_file.read())
|
||||
if error_match_obj:
|
||||
log.debug(
|
||||
"The error could be: `%s`",
|
||||
error_match_obj.group()
|
||||
)
|
||||
raise LatexError()
|
||||
|
||||
# dvi to svg
|
||||
os.system(" ".join((
|
||||
"dvisvgm",
|
||||
f"\"{root}{dvi_ext}\"",
|
||||
"-n",
|
||||
"-v",
|
||||
"0",
|
||||
"-o",
|
||||
f"\"{svg_file}\"",
|
||||
">",
|
||||
os.devnull
|
||||
)))
|
||||
|
||||
# Cleanup superfluous documents
|
||||
tex_dir, name = os.path.split(svg_file)
|
||||
stem, end = name.split(".")
|
||||
for file in filter(lambda s: s.startswith(stem), os.listdir(tex_dir)):
|
||||
if not file.endswith(end):
|
||||
os.remove(os.path.join(tex_dir, file))
|
||||
|
||||
return svg_file
|
||||
|
||||
|
||||
def tex_to_dvi(tex_file: str) -> str:
|
||||
tex_config = get_tex_config()
|
||||
program = tex_config["executable"]
|
||||
file_type = tex_config["intermediate_filetype"]
|
||||
result = tex_file.replace(".tex", "." + file_type)
|
||||
if not os.path.exists(result):
|
||||
commands = [
|
||||
program,
|
||||
"-interaction=batchmode",
|
||||
"-halt-on-error",
|
||||
f"-output-directory=\"{os.path.dirname(tex_file)}\"",
|
||||
f"\"{tex_file}\"",
|
||||
">",
|
||||
os.devnull
|
||||
]
|
||||
exit_code = os.system(" ".join(commands))
|
||||
if exit_code != 0:
|
||||
log_file = tex_file.replace(".tex", ".log")
|
||||
log.error("LaTeX Error! Not a worry, it happens to the best of us.")
|
||||
error_str = ""
|
||||
with open(log_file, "r", encoding="utf-8") as file:
|
||||
for line in file.readlines():
|
||||
if line.startswith("!"):
|
||||
error_str = line[2:-1]
|
||||
log.debug(f"The error could be: `{error_str}`")
|
||||
raise LatexError(error_str)
|
||||
return result
|
||||
|
||||
|
||||
def dvi_to_svg(dvi_file: str) -> str:
|
||||
"""
|
||||
Converts a dvi, which potentially has multiple slides, into a
|
||||
directory full of enumerated pngs corresponding with these slides.
|
||||
Returns a list of PIL Image objects for these images sorted as they
|
||||
where in the dvi
|
||||
"""
|
||||
file_type = get_tex_config()["intermediate_filetype"]
|
||||
result = dvi_file.replace("." + file_type, ".svg")
|
||||
if not os.path.exists(result):
|
||||
commands = [
|
||||
"dvisvgm",
|
||||
"\"{}\"".format(dvi_file),
|
||||
"-n",
|
||||
"-v",
|
||||
"0",
|
||||
"-o",
|
||||
"\"{}\"".format(result),
|
||||
">",
|
||||
os.devnull
|
||||
]
|
||||
os.system(" ".join(commands))
|
||||
return result
|
||||
for ext in (".tex", dvi_ext, ".log", ".aux"):
|
||||
try:
|
||||
os.remove(root + ext)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
# TODO, perhaps this should live elsewhere
|
||||
@contextmanager
|
||||
def display_during_execution(message: str) -> None:
|
||||
# Only show top line
|
||||
to_print = message.split("\n")[0]
|
||||
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] + "..."
|
||||
@ -140,6 +157,5 @@ def display_during_execution(message: str) -> None:
|
||||
print(" " * len(to_print), end="\r")
|
||||
|
||||
|
||||
|
||||
class LatexError(Exception):
|
||||
pass
|
||||
|
@ -17,7 +17,7 @@ rich
|
||||
scipy
|
||||
screeninfo
|
||||
skia-pathops
|
||||
svgelements
|
||||
svgelements>=1.8.1
|
||||
sympy
|
||||
tqdm
|
||||
validators
|
||||
|
Reference in New Issue
Block a user