from functools import reduce import operator as op import re import itertools as it from manimlib.constants import * from manimlib.mobject.geometry import Line from manimlib.mobject.svg.svg_mobject import SVGMobject from manimlib.mobject.svg.svg_mobject import VMobjectFromSVGPathstring from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VectorizedPoint from manimlib.utils.config_ops import digest_config from manimlib.utils.tex_file_writing import tex_to_svg_file from manimlib.utils.tex_file_writing import get_tex_config from manimlib.utils.tex_file_writing import display_during_execution SCALE_FACTOR_PER_FONT_POINT = 0.001 class TexSymbol(VMobjectFromSVGPathstring): CONFIG = { "should_subdivide_sharp_curves": True, "should_remove_null_curves": True, } class SingleStringTexMobject(SVGMobject): CONFIG = { "fill_opacity": 1.0, "stroke_width": 0, "should_center": True, "font_size": 48, "height": None, "organize_left_to_right": False, "alignment": "", # Could be "\\centering", "math_mode": True, } def __init__(self, tex_string, **kwargs): digest_config(self, kwargs) assert(isinstance(tex_string, str)) self.tex_string = tex_string file_name = tex_to_svg_file( self.get_tex_file_body(tex_string), ) SVGMobject.__init__(self, file_name=file_name, **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() def get_tex_file_body(self, tex_string): new_tex = self.get_modified_expression(tex_string) if self.math_mode: new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}" tex_config = get_tex_config() return tex_config["tex_body"].replace( tex_config["text_to_replace"], new_tex ) def get_modified_expression(self, tex_string): result = self.alignment + " " + tex_string result = result.strip() result = self.modify_special_strings(result) return result def modify_special_strings(self, tex): 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 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): """ Makes TexMobject resiliant to unmatched { at start """ num_lefts, num_rights = [tex.count(char) for char in "{}"] while num_rights > num_lefts: tex = "{" + tex num_lefts += 1 while num_lefts > num_rights: tex = tex + "}" num_rights += 1 return tex def get_tex(self): return self.tex_string def path_string_to_mobject(self, path_string): # Overwrite superclass default to use # specialized path_string mobject return TexSymbol(path_string) def organize_submobjects_left_to_right(self): self.sort(lambda p: p[0]) return self class TexMobject(SingleStringTexMobject): CONFIG = { "arg_separator": " ", # Note, use of isolate is largely rendered # moot by the fact that you can surround such strings in # {{ and }} as needed. "isolate": [], "tex_to_color_map": {}, } def __init__(self, *tex_strings, **kwargs): digest_config(self, kwargs) self.tex_strings = self.break_up_tex_strings(tex_strings) full_string = self.arg_separator.join(self.tex_strings) with display_during_execution(f" Writing \"{full_string}\""): super().__init__(full_string, **kwargs) self.break_up_by_substrings() self.set_color_by_tex_to_color_map(self.tex_to_color_map) if self.organize_left_to_right: self.organize_submobjects_left_to_right() def break_up_tex_strings(self, tex_strings): # Separate out anything surrounded in double braces patterns = ["{{", "}}"] # Separate out any strings specified in the isolate # or tex_to_color_map lists. patterns.extend([ "({})".format(re.escape(ss)) for ss in it.chain(self.isolate, self.tex_to_color_map.keys()) ]) pattern = "|".join(patterns) pieces = [] for s in tex_strings: pieces.extend(re.split(pattern, s)) return list(filter(lambda s: s, pieces)) def break_up_by_substrings(self): """ Reorganize existing submojects one layer deeper based on the structure of tex_strings (as a list of tex_strings) """ if len(self.tex_strings) == 1: submob = self.copy() self.set_submobjects([submob]) return self new_submobjects = [] curr_index = 0 config = dict(self.CONFIG) config["alignment"] = "" for tex_string in self.tex_strings: tex_string = tex_string.strip() if len(tex_string) == 0: continue sub_tex_mob = SingleStringTexMobject(tex_string, **config) num_submobs = len(sub_tex_mob) if num_submobs == 0: continue new_index = curr_index + num_submobs sub_tex_mob.set_submobjects(self[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, substring=True, case_sensitive=True): 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, SingleStringTexMobject) and test(tex, m.get_tex()), self.submobjects )) def get_part_by_tex(self, tex, **kwargs): 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, color, **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, **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, start=0): return self.submobjects.index(part, start) def index_of_part_by_tex(self, tex, start=0, **kwargs): part = self.get_part_by_tex(tex, **kwargs) return self.index_of_part(part, start) def slice_by_tex(self, start_tex=None, stop_tex=None, **kwargs): 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): self.submobjects.sort(key=lambda m: m.get_tex()) def set_bstroke(self, color=BLACK, width=4): self.set_stroke(color, width, background=True) return self class TextMobject(TexMobject): CONFIG = { "math_mode": False, "arg_separator": "", } class BulletedList(TextMobject): CONFIG = { "buff": MED_LARGE_BUFF, "dot_scale_factor": 2, "alignment": "", } def __init__(self, *items, **kwargs): line_separated_items = [s + "\\\\" for s in items] TextMobject.__init__(self, *line_separated_items, **kwargs) for part in self: dot = TexMobject("\\cdot").scale(self.dot_scale_factor) dot.next_to(part[0], LEFT, SMALL_BUFF) part.add_to_back(dot) self.arrange( DOWN, aligned_edge=LEFT, buff=self.buff ) def fade_all_but(self, index_or_string, opacity=0.5): arg = index_or_string if isinstance(arg, str): part = self.get_part_by_tex(arg) elif isinstance(arg, int): part = self.submobjects[arg] else: raise Exception("Expected int or string, got {0}".format(arg)) for other_part in self.submobjects: if other_part is part: other_part.set_fill(opacity=1) else: other_part.set_fill(opacity=opacity) class TexMobjectFromPresetString(TexMobject): CONFIG = { # To be filled by subclasses "tex": None, "color": None, } def __init__(self, **kwargs): digest_config(self, kwargs) TexMobject.__init__(self, self.tex, **kwargs) self.set_color(self.color) class Title(TextMobject): CONFIG = { "scale_factor": 1, "include_underline": True, "underline_width": FRAME_WIDTH - 2, # This will override underline_width "match_underline_width_to_text": False, "underline_buff": MED_SMALL_BUFF, } def __init__(self, *text_parts, **kwargs): TextMobject.__init__(self, *text_parts, **kwargs) self.scale(self.scale_factor) self.to_edge(UP) if self.include_underline: underline = Line(LEFT, RIGHT) underline.next_to(self, DOWN, buff=self.underline_buff) if self.match_underline_width_to_text: underline.match_width(self) else: underline.set_width(self.underline_width) self.add(underline) self.underline = underline