old_tex_mobject.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. from __future__ import annotations
  2. from functools import reduce
  3. import operator as op
  4. import re
  5. from manimlib.constants import BLACK, WHITE
  6. from manimlib.mobject.svg.svg_mobject import SVGMobject
  7. from manimlib.mobject.types.vectorized_mobject import VGroup
  8. from manimlib.utils.tex_file_writing import tex_content_to_svg_file
  9. from typing import TYPE_CHECKING
  10. if TYPE_CHECKING:
  11. from typing import Iterable, List, Dict
  12. from manimlib.typing import ManimColor
  13. SCALE_FACTOR_PER_FONT_POINT = 0.001
  14. class SingleStringTex(SVGMobject):
  15. height: float | None = None
  16. def __init__(
  17. self,
  18. tex_string: str,
  19. height: float | None = None,
  20. fill_color: ManimColor = WHITE,
  21. fill_opacity: float = 1.0,
  22. stroke_width: float = 0,
  23. svg_default: dict = dict(fill_color=WHITE),
  24. path_string_config: dict = dict(),
  25. font_size: int = 48,
  26. alignment: str = R"\centering",
  27. math_mode: bool = True,
  28. organize_left_to_right: bool = False,
  29. template: str = "",
  30. additional_preamble: str = "",
  31. **kwargs
  32. ):
  33. self.tex_string = tex_string
  34. self.svg_default = dict(svg_default)
  35. self.path_string_config = dict(path_string_config)
  36. self.font_size = font_size
  37. self.alignment = alignment
  38. self.math_mode = math_mode
  39. self.organize_left_to_right = organize_left_to_right
  40. self.template = template
  41. self.additional_preamble = additional_preamble
  42. super().__init__(
  43. height=height,
  44. fill_color=fill_color,
  45. fill_opacity=fill_opacity,
  46. stroke_width=stroke_width,
  47. path_string_config=path_string_config,
  48. **kwargs
  49. )
  50. if self.height is None:
  51. self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
  52. if self.organize_left_to_right:
  53. self.organize_submobjects_left_to_right()
  54. @property
  55. def hash_seed(self) -> tuple:
  56. return (
  57. self.__class__.__name__,
  58. self.svg_default,
  59. self.path_string_config,
  60. self.tex_string,
  61. self.alignment,
  62. self.math_mode,
  63. self.template,
  64. self.additional_preamble
  65. )
  66. def get_file_path(self) -> str:
  67. content = self.get_tex_file_body(self.tex_string)
  68. file_path = tex_content_to_svg_file(
  69. content, self.template, self.additional_preamble, self.tex_string
  70. )
  71. return file_path
  72. def get_tex_file_body(self, tex_string: str) -> str:
  73. new_tex = self.get_modified_expression(tex_string)
  74. if self.math_mode:
  75. new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
  76. return self.alignment + "\n" + new_tex
  77. def get_modified_expression(self, tex_string: str) -> str:
  78. return self.modify_special_strings(tex_string.strip())
  79. def modify_special_strings(self, tex: str) -> str:
  80. tex = tex.strip()
  81. should_add_filler = reduce(op.or_, [
  82. # Fraction line needs something to be over
  83. tex == "\\over",
  84. tex == "\\overline",
  85. # Makesure sqrt has overbar
  86. tex == "\\sqrt",
  87. tex == "\\sqrt{",
  88. # Need to add blank subscript or superscript
  89. tex.endswith("_"),
  90. tex.endswith("^"),
  91. tex.endswith("dot"),
  92. ])
  93. if should_add_filler:
  94. filler = "{\\quad}"
  95. tex += filler
  96. should_add_double_filler = reduce(op.or_, [
  97. tex == "\\overset",
  98. # TODO: these can't be used since they change
  99. # the latex draw order.
  100. # tex == "\\frac", # you can use \\over as a alternative
  101. # tex == "\\dfrac",
  102. # tex == "\\binom",
  103. ])
  104. if should_add_double_filler:
  105. filler = "{\\quad}{\\quad}"
  106. tex += filler
  107. if tex == "\\substack":
  108. tex = "\\quad"
  109. if tex == "":
  110. tex = "\\quad"
  111. # To keep files from starting with a line break
  112. if tex.startswith("\\\\"):
  113. tex = tex.replace("\\\\", "\\quad\\\\")
  114. tex = self.balance_braces(tex)
  115. # Handle imbalanced \left and \right
  116. num_lefts, num_rights = [
  117. len([
  118. s for s in tex.split(substr)[1:]
  119. if s and s[0] in "(){}[]|.\\"
  120. ])
  121. for substr in ("\\left", "\\right")
  122. ]
  123. if num_lefts != num_rights:
  124. tex = tex.replace("\\left", "\\big")
  125. tex = tex.replace("\\right", "\\big")
  126. for context in ["array"]:
  127. begin_in = ("\\begin{%s}" % context) in tex
  128. end_in = ("\\end{%s}" % context) in tex
  129. if begin_in ^ end_in:
  130. # Just turn this into a blank string,
  131. # which means caller should leave a
  132. # stray \\begin{...} with other symbols
  133. tex = ""
  134. return tex
  135. def balance_braces(self, tex: str) -> str:
  136. """
  137. Makes Tex resiliant to unmatched braces
  138. """
  139. num_unclosed_brackets = 0
  140. for i in range(len(tex)):
  141. if i > 0 and tex[i - 1] == "\\":
  142. # So as to not count '\{' type expressions
  143. continue
  144. char = tex[i]
  145. if char == "{":
  146. num_unclosed_brackets += 1
  147. elif char == "}":
  148. if num_unclosed_brackets == 0:
  149. tex = "{" + tex
  150. else:
  151. num_unclosed_brackets -= 1
  152. tex += num_unclosed_brackets * "}"
  153. return tex
  154. def get_tex(self) -> str:
  155. return self.tex_string
  156. def organize_submobjects_left_to_right(self):
  157. self.sort(lambda p: p[0])
  158. return self
  159. class OldTex(SingleStringTex):
  160. def __init__(
  161. self,
  162. *tex_strings: str,
  163. arg_separator: str = "",
  164. isolate: List[str] = [],
  165. tex_to_color_map: Dict[str, ManimColor] = {},
  166. **kwargs
  167. ):
  168. self.tex_strings = self.break_up_tex_strings(
  169. tex_strings,
  170. substrings_to_isolate=[*isolate, *tex_to_color_map.keys()]
  171. )
  172. full_string = arg_separator.join(self.tex_strings)
  173. super().__init__(full_string, **kwargs)
  174. self.break_up_by_substrings(self.tex_strings)
  175. self.set_color_by_tex_to_color_map(tex_to_color_map)
  176. if self.organize_left_to_right:
  177. self.organize_submobjects_left_to_right()
  178. def break_up_tex_strings(self, tex_strings: Iterable[str], substrings_to_isolate: List[str] = []) -> Iterable[str]:
  179. # Separate out any strings specified in the isolate
  180. # or tex_to_color_map lists.
  181. if len(substrings_to_isolate) == 0:
  182. return tex_strings
  183. patterns = (
  184. "({})".format(re.escape(ss))
  185. for ss in substrings_to_isolate
  186. )
  187. pattern = "|".join(patterns)
  188. pieces = []
  189. for s in tex_strings:
  190. if pattern:
  191. pieces.extend(re.split(pattern, s))
  192. else:
  193. pieces.append(s)
  194. return list(filter(lambda s: s, pieces))
  195. def break_up_by_substrings(self, tex_strings: Iterable[str]):
  196. """
  197. Reorganize existing submojects one layer
  198. deeper based on the structure of tex_strings (as a list
  199. of tex_strings)
  200. """
  201. if len(list(tex_strings)) == 1:
  202. submob = self.copy()
  203. self.set_submobjects([submob])
  204. return self
  205. new_submobjects = []
  206. curr_index = 0
  207. for tex_string in tex_strings:
  208. tex_string = tex_string.strip()
  209. if len(tex_string) == 0:
  210. continue
  211. sub_tex_mob = SingleStringTex(tex_string, math_mode=self.math_mode)
  212. num_submobs = len(sub_tex_mob)
  213. if num_submobs == 0:
  214. continue
  215. new_index = curr_index + num_submobs
  216. sub_tex_mob.set_submobjects(self.submobjects[curr_index:new_index])
  217. new_submobjects.append(sub_tex_mob)
  218. curr_index = new_index
  219. self.set_submobjects(new_submobjects)
  220. return self
  221. def get_parts_by_tex(
  222. self,
  223. tex: str,
  224. substring: bool = True,
  225. case_sensitive: bool = True
  226. ) -> VGroup:
  227. def test(tex1, tex2):
  228. if not case_sensitive:
  229. tex1 = tex1.lower()
  230. tex2 = tex2.lower()
  231. if substring:
  232. return tex1 in tex2
  233. else:
  234. return tex1 == tex2
  235. return VGroup(*filter(
  236. lambda m: isinstance(m, SingleStringTex) and test(tex, m.get_tex()),
  237. self.submobjects
  238. ))
  239. def get_part_by_tex(self, tex: str, **kwargs) -> SingleStringTex | None:
  240. all_parts = self.get_parts_by_tex(tex, **kwargs)
  241. return all_parts[0] if all_parts else None
  242. def set_color_by_tex(self, tex: str, color: ManimColor, **kwargs):
  243. self.get_parts_by_tex(tex, **kwargs).set_color(color)
  244. return self
  245. def set_color_by_tex_to_color_map(
  246. self,
  247. tex_to_color_map: dict[str, ManimColor],
  248. **kwargs
  249. ):
  250. for tex, color in list(tex_to_color_map.items()):
  251. self.set_color_by_tex(tex, color, **kwargs)
  252. return self
  253. def index_of_part(self, part: SingleStringTex, start: int = 0) -> int:
  254. return self.submobjects.index(part, start)
  255. def index_of_part_by_tex(self, tex: str, start: int = 0, **kwargs) -> int:
  256. part = self.get_part_by_tex(tex, **kwargs)
  257. return self.index_of_part(part, start)
  258. def slice_by_tex(
  259. self,
  260. start_tex: str | None = None,
  261. stop_tex: str | None = None,
  262. **kwargs
  263. ) -> VGroup:
  264. if start_tex is None:
  265. start_index = 0
  266. else:
  267. start_index = self.index_of_part_by_tex(start_tex, **kwargs)
  268. if stop_tex is None:
  269. return self[start_index:]
  270. else:
  271. stop_index = self.index_of_part_by_tex(stop_tex, start=start_index, **kwargs)
  272. return self[start_index:stop_index]
  273. def sort_alphabetically(self) -> None:
  274. self.submobjects.sort(key=lambda m: m.get_tex())
  275. def set_bstroke(self, color: ManimColor = BLACK, width: float = 4):
  276. self.set_stroke(color, width, background=True)
  277. return self
  278. class OldTexText(OldTex):
  279. def __init__(
  280. self,
  281. *tex_strings: str,
  282. math_mode: bool = False,
  283. arg_separator: str = "",
  284. **kwargs
  285. ):
  286. super().__init__(
  287. *tex_strings,
  288. math_mode=math_mode,
  289. arg_separator=arg_separator,
  290. **kwargs
  291. )