123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- from __future__ import annotations
- from functools import reduce
- import operator as op
- import re
- from manimlib.constants import BLACK, WHITE
- from manimlib.mobject.svg.svg_mobject import SVGMobject
- from manimlib.mobject.types.vectorized_mobject import VGroup
- from manimlib.utils.tex_file_writing import tex_content_to_svg_file
- from typing import TYPE_CHECKING
- if TYPE_CHECKING:
- from typing import Iterable, List, Dict
- from manimlib.typing import ManimColor
- SCALE_FACTOR_PER_FONT_POINT = 0.001
- class SingleStringTex(SVGMobject):
- height: float | None = None
- def __init__(
- self,
- tex_string: str,
- height: float | None = None,
- fill_color: ManimColor = WHITE,
- fill_opacity: float = 1.0,
- stroke_width: float = 0,
- svg_default: dict = dict(fill_color=WHITE),
- path_string_config: dict = dict(),
- font_size: int = 48,
- alignment: str = R"\centering",
- math_mode: bool = True,
- organize_left_to_right: bool = False,
- template: str = "",
- additional_preamble: str = "",
- **kwargs
- ):
- self.tex_string = tex_string
- self.svg_default = dict(svg_default)
- self.path_string_config = dict(path_string_config)
- self.font_size = font_size
- self.alignment = alignment
- self.math_mode = math_mode
- self.organize_left_to_right = organize_left_to_right
- self.template = template
- self.additional_preamble = additional_preamble
- super().__init__(
- height=height,
- fill_color=fill_color,
- fill_opacity=fill_opacity,
- stroke_width=stroke_width,
- path_string_config=path_string_config,
- **kwargs
- )
- if self.height is None:
- self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
- if self.organize_left_to_right:
- self.organize_submobjects_left_to_right()
- @property
- def hash_seed(self) -> tuple:
- return (
- self.__class__.__name__,
- self.svg_default,
- self.path_string_config,
- self.tex_string,
- self.alignment,
- self.math_mode,
- self.template,
- self.additional_preamble
- )
- def get_file_path(self) -> str:
- content = self.get_tex_file_body(self.tex_string)
- file_path = tex_content_to_svg_file(
- content, self.template, self.additional_preamble, self.tex_string
- )
- return file_path
- def get_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*}"
- return self.alignment + "\n" + new_tex
- def get_modified_expression(self, tex_string: str) -> str:
- return self.modify_special_strings(tex_string.strip())
- def modify_special_strings(self, tex: str) -> str:
- tex = tex.strip()
- should_add_filler = reduce(op.or_, [
- # Fraction line needs something to be over
- tex == "\\over",
- tex == "\\overline",
- # Makesure sqrt has overbar
- tex == "\\sqrt",
- tex == "\\sqrt{",
- # Need to add blank subscript or superscript
- tex.endswith("_"),
- tex.endswith("^"),
- tex.endswith("dot"),
- ])
- if should_add_filler:
- filler = "{\\quad}"
- tex += filler
- should_add_double_filler = reduce(op.or_, [
- tex == "\\overset",
- # TODO: these can't be used since they change
- # the latex draw order.
- # tex == "\\frac", # you can use \\over as a alternative
- # tex == "\\dfrac",
- # tex == "\\binom",
- ])
- if should_add_double_filler:
- filler = "{\\quad}{\\quad}"
- tex += filler
- if tex == "\\substack":
- tex = "\\quad"
- if tex == "":
- tex = "\\quad"
- # To keep files from starting with a line break
- if tex.startswith("\\\\"):
- tex = tex.replace("\\\\", "\\quad\\\\")
- tex = self.balance_braces(tex)
- # Handle imbalanced \left and \right
- num_lefts, num_rights = [
- len([
- s for s in tex.split(substr)[1:]
- if s and s[0] in "(){}[]|.\\"
- ])
- for substr in ("\\left", "\\right")
- ]
- if num_lefts != num_rights:
- tex = tex.replace("\\left", "\\big")
- tex = tex.replace("\\right", "\\big")
- for context in ["array"]:
- begin_in = ("\\begin{%s}" % context) in tex
- end_in = ("\\end{%s}" % context) in tex
- if begin_in ^ end_in:
- # Just turn this into a blank string,
- # which means caller should leave a
- # stray \\begin{...} with other symbols
- tex = ""
- return tex
- def balance_braces(self, tex: str) -> str:
- """
- Makes Tex resiliant to unmatched braces
- """
- num_unclosed_brackets = 0
- for i in range(len(tex)):
- if i > 0 and tex[i - 1] == "\\":
- # So as to not count '\{' type expressions
- continue
- char = tex[i]
- if char == "{":
- num_unclosed_brackets += 1
- elif char == "}":
- if num_unclosed_brackets == 0:
- tex = "{" + tex
- else:
- num_unclosed_brackets -= 1
- tex += num_unclosed_brackets * "}"
- return tex
- def get_tex(self) -> str:
- return self.tex_string
- def organize_submobjects_left_to_right(self):
- self.sort(lambda p: p[0])
- return self
- class OldTex(SingleStringTex):
- def __init__(
- self,
- *tex_strings: str,
- arg_separator: str = "",
- isolate: List[str] = [],
- tex_to_color_map: Dict[str, ManimColor] = {},
- **kwargs
- ):
- self.tex_strings = self.break_up_tex_strings(
- tex_strings,
- substrings_to_isolate=[*isolate, *tex_to_color_map.keys()]
- )
- full_string = arg_separator.join(self.tex_strings)
- super().__init__(full_string, **kwargs)
- self.break_up_by_substrings(self.tex_strings)
- self.set_color_by_tex_to_color_map(tex_to_color_map)
- if self.organize_left_to_right:
- self.organize_submobjects_left_to_right()
- def break_up_tex_strings(self, tex_strings: Iterable[str], substrings_to_isolate: List[str] = []) -> Iterable[str]:
- # Separate out any strings specified in the isolate
- # or tex_to_color_map lists.
- if len(substrings_to_isolate) == 0:
- return tex_strings
- patterns = (
- "({})".format(re.escape(ss))
- for ss in substrings_to_isolate
- )
- pattern = "|".join(patterns)
- pieces = []
- for s in tex_strings:
- if pattern:
- pieces.extend(re.split(pattern, s))
- else:
- pieces.append(s)
- return list(filter(lambda s: s, pieces))
- def break_up_by_substrings(self, tex_strings: Iterable[str]):
- """
- Reorganize existing submojects one layer
- deeper based on the structure of tex_strings (as a list
- of tex_strings)
- """
- if len(list(tex_strings)) == 1:
- submob = self.copy()
- self.set_submobjects([submob])
- return self
- new_submobjects = []
- curr_index = 0
- for tex_string in tex_strings:
- tex_string = tex_string.strip()
- if len(tex_string) == 0:
- continue
- sub_tex_mob = SingleStringTex(tex_string, math_mode=self.math_mode)
- num_submobs = len(sub_tex_mob)
- if num_submobs == 0:
- continue
- new_index = curr_index + num_submobs
- sub_tex_mob.set_submobjects(self.submobjects[curr_index:new_index])
- new_submobjects.append(sub_tex_mob)
- curr_index = new_index
- self.set_submobjects(new_submobjects)
- return self
- def get_parts_by_tex(
- self,
- tex: str,
- substring: bool = True,
- case_sensitive: bool = True
- ) -> VGroup:
- def test(tex1, tex2):
- if not case_sensitive:
- tex1 = tex1.lower()
- tex2 = tex2.lower()
- if substring:
- return tex1 in tex2
- else:
- return tex1 == tex2
- return VGroup(*filter(
- lambda m: isinstance(m, SingleStringTex) and test(tex, m.get_tex()),
- self.submobjects
- ))
- def get_part_by_tex(self, tex: str, **kwargs) -> SingleStringTex | None:
- all_parts = self.get_parts_by_tex(tex, **kwargs)
- return all_parts[0] if all_parts else None
- def set_color_by_tex(self, tex: str, color: ManimColor, **kwargs):
- self.get_parts_by_tex(tex, **kwargs).set_color(color)
- return self
- def set_color_by_tex_to_color_map(
- self,
- tex_to_color_map: dict[str, ManimColor],
- **kwargs
- ):
- for tex, color in list(tex_to_color_map.items()):
- self.set_color_by_tex(tex, color, **kwargs)
- return self
- def index_of_part(self, part: SingleStringTex, start: int = 0) -> int:
- return self.submobjects.index(part, start)
- def index_of_part_by_tex(self, tex: str, start: int = 0, **kwargs) -> int:
- part = self.get_part_by_tex(tex, **kwargs)
- return self.index_of_part(part, start)
- def slice_by_tex(
- self,
- start_tex: str | None = None,
- stop_tex: str | None = None,
- **kwargs
- ) -> VGroup:
- if start_tex is None:
- start_index = 0
- else:
- start_index = self.index_of_part_by_tex(start_tex, **kwargs)
- if stop_tex is None:
- return self[start_index:]
- else:
- stop_index = self.index_of_part_by_tex(stop_tex, start=start_index, **kwargs)
- return self[start_index:stop_index]
- def sort_alphabetically(self) -> None:
- self.submobjects.sort(key=lambda m: m.get_tex())
- def set_bstroke(self, color: ManimColor = BLACK, width: float = 4):
- self.set_stroke(color, width, background=True)
- return self
- class OldTexText(OldTex):
- def __init__(
- self,
- *tex_strings: str,
- math_mode: bool = False,
- arg_separator: str = "",
- **kwargs
- ):
- super().__init__(
- *tex_strings,
- math_mode=math_mode,
- arg_separator=arg_separator,
- **kwargs
- )
|