From da1cc44d9046d9e33fe05b1bec2b58cec7990f0f Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Fri, 12 Nov 2021 21:21:44 +0800 Subject: [PATCH 01/30] Remove `SingleStringTex` --- manimlib/mobject/numbers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index 7a8d05ba..b4850b12 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -1,5 +1,5 @@ from manimlib.constants import * -from manimlib.mobject.svg.tex_mobject import SingleStringTex +from manimlib.mobject.svg.tex_mobject import Tex from manimlib.mobject.svg.text_mobject import Text from manimlib.mobject.types.vectorized_mobject import VMobject @@ -40,7 +40,7 @@ class DecimalNumber(VMobject): dots.arrange(RIGHT, buff=2 * dots[0].get_width()) self.add(dots) if self.unit is not None: - self.unit_sign = self.string_to_mob(self.unit, SingleStringTex) + self.unit_sign = self.string_to_mob(self.unit, Tex) self.add(self.unit_sign) self.arrange( From 1b695e1c19e91bdd3386db6ada6a8d9c0a45170f Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Fri, 12 Nov 2021 21:22:42 +0800 Subject: [PATCH 02/30] Refactor `Tex` --- manimlib/mobject/svg/brace.py | 3 +- manimlib/mobject/svg/tex_mobject.py | 684 ++++++++++++++-------------- 2 files changed, 333 insertions(+), 354 deletions(-) diff --git a/manimlib/mobject/svg/brace.py b/manimlib/mobject/svg/brace.py index cd686fbb..1096f484 100644 --- a/manimlib/mobject/svg/brace.py +++ b/manimlib/mobject/svg/brace.py @@ -6,7 +6,6 @@ from manimlib.constants import * from manimlib.animation.fading import FadeIn from manimlib.animation.growing import GrowFromCenter from manimlib.mobject.svg.tex_mobject import Tex -from manimlib.mobject.svg.tex_mobject import SingleStringTex from manimlib.mobject.svg.tex_mobject import TexText from manimlib.mobject.svg.text_mobject import Text from manimlib.mobject.types.vectorized_mobject import VMobject @@ -14,7 +13,7 @@ from manimlib.utils.config_ops import digest_config from manimlib.utils.space_ops import get_norm -class Brace(SingleStringTex): +class Brace(Tex): CONFIG = { "buff": 0.2, "tex_string": r"\underbrace{\qquad}" diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 6f1054ca..56c05e7a 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -1,352 +1,332 @@ -from functools import reduce -import operator as op -import re - -from manimlib.constants import * -from manimlib.mobject.geometry import Line -from manimlib.mobject.svg.svg_mobject import SVGMobject -from manimlib.mobject.types.vectorized_mobject import VMobject -from manimlib.mobject.types.vectorized_mobject import VGroup -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 - - -tex_string_to_mob_map = {} - - -class SingleStringTex(VMobject): - CONFIG = { - "fill_opacity": 1.0, - "stroke_width": 0, - "should_center": True, - "font_size": 48, - "height": None, - "organize_left_to_right": False, - "alignment": "\\centering", - "math_mode": True, - } - - def __init__(self, tex_string, **kwargs): - super().__init__(**kwargs) - assert(isinstance(tex_string, str)) - self.tex_string = tex_string - if tex_string not in tex_string_to_mob_map: - with display_during_execution(f" Writing \"{tex_string}\""): - full_tex = self.get_tex_file_body(tex_string) - filename = tex_to_svg_file(full_tex) - svg_mob = SVGMobject( - filename, - height=None, - path_string_config={ - "should_subdivide_sharp_curves": True, - "should_remove_null_curves": True, - } - ) - tex_string_to_mob_map[tex_string] = svg_mob - self.add(*( - sm.copy() - for sm in tex_string_to_mob_map[tex_string] - )) - self.init_colors() - - 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*}" - - 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 - ) - - def get_modified_expression(self, tex_string): - return self.modify_special_strings(tex_string.strip()) - - 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 Tex resiliant to unmatched braces - """ - num_unclosed_brackets = 0 - for char in tex: - 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): - return self.tex_string - - def organize_submobjects_left_to_right(self): - self.sort(lambda p: p[0]) - return self - - -class Tex(SingleStringTex): - CONFIG = { - "arg_separator": "", - "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) - 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 any strings specified in the isolate - # or tex_to_color_map lists. - substrings_to_isolate = [*self.isolate, *self.tex_to_color_map.keys()] - 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): - """ - 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 = SingleStringTex(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, SingleStringTex) 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 TexText(Tex): - CONFIG = { - "math_mode": False, - "arg_separator": "", - } - - -class BulletedList(TexText): - CONFIG = { - "buff": MED_LARGE_BUFF, - "dot_scale_factor": 2, - "alignment": "", - } - - def __init__(self, *items, **kwargs): - line_separated_items = [s + "\\\\" for s in items] - TexText.__init__(self, *line_separated_items, **kwargs) - for part in self: - dot = Tex("\\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 TexFromPresetString(Tex): - CONFIG = { - # To be filled by subclasses - "tex": None, - "color": None, - } - - def __init__(self, **kwargs): - digest_config(self, kwargs) - Tex.__init__(self, self.tex, **kwargs) - self.set_color(self.color) - - -class Title(TexText): - 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): - TexText.__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 +from functools import reduce +import hashlib +import operator as op +import re +from types import MethodType + +from manimlib.constants import * +from manimlib.mobject.geometry import Line +from manimlib.mobject.svg.svg_mobject import SVGMobject +from manimlib.mobject.types.vectorized_mobject import VMobject +from manimlib.mobject.types.vectorized_mobject import VGroup +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 + + +tex_hash_to_mob_map = {} + + +class SVGTex(SVGMobject): + CONFIG = { + "height": None, + # The hierachy structure is needed for the `break_up_by_substrings` method + "unpack_groups": False, + "path_string_config": { + "should_subdivide_sharp_curves": True, + "should_remove_null_curves": True, + } + } + + def __init__(self, tex_obj, **kwargs): + self.tex_obj = tex_obj + full_tex = self.get_tex_file_body() + filename = tex_to_svg_file(full_tex) + super().__init__(filename, **kwargs) + self.break_up_by_substrings() + self.init_colors() + + def get_mobjects_from(self, element): + result = super().get_mobjects_from(element) + if len(result) == 0: + return result + result[0].fill_color = None + try: + fill_color = element.getAttribute("fill") + if fill_color: + result[0].fill_color = fill_color + except: + pass + return result + + def get_tex_file_body(self): + new_tex = self.get_modified_expression() + if self.tex_obj.math_mode: + new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}" + + new_tex = self.tex_obj.alignment + "\n" + new_tex + + tex_config = get_tex_config() + return tex_config["tex_body"].replace( + tex_config["text_to_replace"], + new_tex + ) + + def get_modified_expression(self): + tex_components = [] + to_isolate = self.tex_obj.substrings_to_isolate + for tex_substring in self.tex_obj.tex_substrings: + if tex_substring not in to_isolate: + tex_components.append(tex_substring) + else: + color_index = to_isolate.index(tex_substring) + tex_components.append("".join([ + "{\\color[RGB]{", + str(self.get_nth_color_tuple(color_index))[1:-1], + "}", + tex_substring, + "}" + ])) + return self.tex_obj.arg_separator.join(tex_components) + + def break_up_by_substrings(self): + """ + Reorganize existing submojects one layer + deeper based on the structure of tex_substrings (as a list + of tex_substrings) + """ + if len(self.tex_obj.tex_substrings) == 1: + submob = self.copy() + self.set_submobjects([submob]) + return self + new_submobjects = [] + new_submobject_components = [] + for part in self.submobjects: + if part.fill_color is not None: + if new_submobject_components: + new_submobjects.append(VGroup(*new_submobject_components)) + new_submobject_components = [] + new_submobjects.append(part) + else: + new_submobject_components.append(part) + if new_submobject_components: + new_submobjects.append(VGroup(*new_submobject_components)) + + for submob, tex_substring in zip(new_submobjects, self.tex_obj.tex_substrings): + fill_color = submob.fill_color + if fill_color is not None: + submob_tex_string = self.tex_obj.substrings_to_isolate[int(fill_color[1:], 16) - 1] + else: + submob_tex_string = tex_substring + submob.tex_string = submob_tex_string + # Prevent methods and classes using `get_tex()` from breaking. + submob.get_tex = MethodType(lambda sm: sm.tex_string, submob) + self.set_submobjects(new_submobjects) + + return self + + @staticmethod + def get_nth_color_tuple(n): ## TODO: Refactor + # Get a unique color different from black, + # or the svg file will not include the color information. + return ( + (n + 1) // 256 // 256, + (n + 1) // 256 % 256, + (n + 1) % 256 + ) + + +class Tex(VMobject): + CONFIG = { + "fill_opacity": 1.0, + "stroke_width": 0, + "should_center": True, + "font_size": 48, + "height": None, + "organize_left_to_right": False, + "alignment": "\\centering", + "math_mode": True, + "arg_separator": "", + "isolate": [], + "tex_to_color_map": {}, + } + + def __init__(self, *tex_strings, **kwargs): + super().__init__(**kwargs) + self.substrings_to_isolate = [*self.isolate, *self.tex_to_color_map.keys()] + self.tex_substrings = self.break_up_tex_strings(tex_strings) + tex_string = self.arg_separator.join(tex_strings) + self.tex_string = tex_string + + hash_val = self.tex2hash() + if hash_val not in tex_hash_to_mob_map: ## TODO + with display_during_execution(f" Writing \"{tex_string}\""): + svg_mob = SVGTex(self) + tex_hash_to_mob_map[hash_val] = svg_mob + self.add(*( + sm.copy() + for sm in tex_hash_to_mob_map[hash_val] + )) + self.init_colors() + self.set_color_by_tex_to_color_map(self.tex_to_color_map, substring=False) + + 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 tex2hash(self): + id_str = self.tex_string + str(self.substrings_to_isolate) + hasher = hashlib.sha256() + hasher.update(id_str.encode()) + return hasher.hexdigest()[:16] + + def break_up_tex_strings(self, tex_strings): + # Separate out any strings specified in the isolate + # or tex_to_color_map lists. + if len(self.substrings_to_isolate) == 0: + return tex_strings + patterns = ( + "({})".format(re.escape(ss)) + for ss in self.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 organize_submobjects_left_to_right(self): + self.sort(lambda p: p[0]) + 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(*[ + mob for mob in self.submobjects if test(tex, mob.get_tex()) + ]) + + 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 TexText(Tex): + CONFIG = { + "math_mode": False, + "arg_separator": "", + } + + +class BulletedList(TexText): + CONFIG = { + "buff": MED_LARGE_BUFF, + "dot_scale_factor": 2, + "alignment": "", + } + + def __init__(self, *items, **kwargs): + line_separated_items = [s + "\\\\" for s in items] + TexText.__init__(self, *line_separated_items, **kwargs) + for part in self: + dot = Tex("\\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 TexFromPresetString(Tex): + CONFIG = { + # To be filled by subclasses + "tex": None, + "color": None, + } + + def __init__(self, **kwargs): + digest_config(self, kwargs) + Tex.__init__(self, self.tex, **kwargs) + self.set_color(self.color) + + +class Title(TexText): + 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): + TexText.__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 From 5c2a9f212945e0129caf54120ffd7dfc9e9df9f3 Mon Sep 17 00:00:00 2001 From: TonyCrane Date: Fri, 12 Nov 2021 21:49:56 +0800 Subject: [PATCH 03/30] style: change CRLF to LF Change the line ending characters from CRLF to LF --- manimlib/mobject/svg/tex_mobject.py | 664 ++++++++++++++-------------- 1 file changed, 332 insertions(+), 332 deletions(-) diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 56c05e7a..1489ee1a 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -1,332 +1,332 @@ -from functools import reduce -import hashlib -import operator as op -import re -from types import MethodType - -from manimlib.constants import * -from manimlib.mobject.geometry import Line -from manimlib.mobject.svg.svg_mobject import SVGMobject -from manimlib.mobject.types.vectorized_mobject import VMobject -from manimlib.mobject.types.vectorized_mobject import VGroup -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 - - -tex_hash_to_mob_map = {} - - -class SVGTex(SVGMobject): - CONFIG = { - "height": None, - # The hierachy structure is needed for the `break_up_by_substrings` method - "unpack_groups": False, - "path_string_config": { - "should_subdivide_sharp_curves": True, - "should_remove_null_curves": True, - } - } - - def __init__(self, tex_obj, **kwargs): - self.tex_obj = tex_obj - full_tex = self.get_tex_file_body() - filename = tex_to_svg_file(full_tex) - super().__init__(filename, **kwargs) - self.break_up_by_substrings() - self.init_colors() - - def get_mobjects_from(self, element): - result = super().get_mobjects_from(element) - if len(result) == 0: - return result - result[0].fill_color = None - try: - fill_color = element.getAttribute("fill") - if fill_color: - result[0].fill_color = fill_color - except: - pass - return result - - def get_tex_file_body(self): - new_tex = self.get_modified_expression() - if self.tex_obj.math_mode: - new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}" - - new_tex = self.tex_obj.alignment + "\n" + new_tex - - tex_config = get_tex_config() - return tex_config["tex_body"].replace( - tex_config["text_to_replace"], - new_tex - ) - - def get_modified_expression(self): - tex_components = [] - to_isolate = self.tex_obj.substrings_to_isolate - for tex_substring in self.tex_obj.tex_substrings: - if tex_substring not in to_isolate: - tex_components.append(tex_substring) - else: - color_index = to_isolate.index(tex_substring) - tex_components.append("".join([ - "{\\color[RGB]{", - str(self.get_nth_color_tuple(color_index))[1:-1], - "}", - tex_substring, - "}" - ])) - return self.tex_obj.arg_separator.join(tex_components) - - def break_up_by_substrings(self): - """ - Reorganize existing submojects one layer - deeper based on the structure of tex_substrings (as a list - of tex_substrings) - """ - if len(self.tex_obj.tex_substrings) == 1: - submob = self.copy() - self.set_submobjects([submob]) - return self - new_submobjects = [] - new_submobject_components = [] - for part in self.submobjects: - if part.fill_color is not None: - if new_submobject_components: - new_submobjects.append(VGroup(*new_submobject_components)) - new_submobject_components = [] - new_submobjects.append(part) - else: - new_submobject_components.append(part) - if new_submobject_components: - new_submobjects.append(VGroup(*new_submobject_components)) - - for submob, tex_substring in zip(new_submobjects, self.tex_obj.tex_substrings): - fill_color = submob.fill_color - if fill_color is not None: - submob_tex_string = self.tex_obj.substrings_to_isolate[int(fill_color[1:], 16) - 1] - else: - submob_tex_string = tex_substring - submob.tex_string = submob_tex_string - # Prevent methods and classes using `get_tex()` from breaking. - submob.get_tex = MethodType(lambda sm: sm.tex_string, submob) - self.set_submobjects(new_submobjects) - - return self - - @staticmethod - def get_nth_color_tuple(n): ## TODO: Refactor - # Get a unique color different from black, - # or the svg file will not include the color information. - return ( - (n + 1) // 256 // 256, - (n + 1) // 256 % 256, - (n + 1) % 256 - ) - - -class Tex(VMobject): - CONFIG = { - "fill_opacity": 1.0, - "stroke_width": 0, - "should_center": True, - "font_size": 48, - "height": None, - "organize_left_to_right": False, - "alignment": "\\centering", - "math_mode": True, - "arg_separator": "", - "isolate": [], - "tex_to_color_map": {}, - } - - def __init__(self, *tex_strings, **kwargs): - super().__init__(**kwargs) - self.substrings_to_isolate = [*self.isolate, *self.tex_to_color_map.keys()] - self.tex_substrings = self.break_up_tex_strings(tex_strings) - tex_string = self.arg_separator.join(tex_strings) - self.tex_string = tex_string - - hash_val = self.tex2hash() - if hash_val not in tex_hash_to_mob_map: ## TODO - with display_during_execution(f" Writing \"{tex_string}\""): - svg_mob = SVGTex(self) - tex_hash_to_mob_map[hash_val] = svg_mob - self.add(*( - sm.copy() - for sm in tex_hash_to_mob_map[hash_val] - )) - self.init_colors() - self.set_color_by_tex_to_color_map(self.tex_to_color_map, substring=False) - - 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 tex2hash(self): - id_str = self.tex_string + str(self.substrings_to_isolate) - hasher = hashlib.sha256() - hasher.update(id_str.encode()) - return hasher.hexdigest()[:16] - - def break_up_tex_strings(self, tex_strings): - # Separate out any strings specified in the isolate - # or tex_to_color_map lists. - if len(self.substrings_to_isolate) == 0: - return tex_strings - patterns = ( - "({})".format(re.escape(ss)) - for ss in self.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 organize_submobjects_left_to_right(self): - self.sort(lambda p: p[0]) - 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(*[ - mob for mob in self.submobjects if test(tex, mob.get_tex()) - ]) - - 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 TexText(Tex): - CONFIG = { - "math_mode": False, - "arg_separator": "", - } - - -class BulletedList(TexText): - CONFIG = { - "buff": MED_LARGE_BUFF, - "dot_scale_factor": 2, - "alignment": "", - } - - def __init__(self, *items, **kwargs): - line_separated_items = [s + "\\\\" for s in items] - TexText.__init__(self, *line_separated_items, **kwargs) - for part in self: - dot = Tex("\\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 TexFromPresetString(Tex): - CONFIG = { - # To be filled by subclasses - "tex": None, - "color": None, - } - - def __init__(self, **kwargs): - digest_config(self, kwargs) - Tex.__init__(self, self.tex, **kwargs) - self.set_color(self.color) - - -class Title(TexText): - 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): - TexText.__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 +from functools import reduce +import hashlib +import operator as op +import re +from types import MethodType + +from manimlib.constants import * +from manimlib.mobject.geometry import Line +from manimlib.mobject.svg.svg_mobject import SVGMobject +from manimlib.mobject.types.vectorized_mobject import VMobject +from manimlib.mobject.types.vectorized_mobject import VGroup +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 + + +tex_hash_to_mob_map = {} + + +class SVGTex(SVGMobject): + CONFIG = { + "height": None, + # The hierachy structure is needed for the `break_up_by_substrings` method + "unpack_groups": False, + "path_string_config": { + "should_subdivide_sharp_curves": True, + "should_remove_null_curves": True, + } + } + + def __init__(self, tex_obj, **kwargs): + self.tex_obj = tex_obj + full_tex = self.get_tex_file_body() + filename = tex_to_svg_file(full_tex) + super().__init__(filename, **kwargs) + self.break_up_by_substrings() + self.init_colors() + + def get_mobjects_from(self, element): + result = super().get_mobjects_from(element) + if len(result) == 0: + return result + result[0].fill_color = None + try: + fill_color = element.getAttribute("fill") + if fill_color: + result[0].fill_color = fill_color + except: + pass + return result + + def get_tex_file_body(self): + new_tex = self.get_modified_expression() + if self.tex_obj.math_mode: + new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}" + + new_tex = self.tex_obj.alignment + "\n" + new_tex + + tex_config = get_tex_config() + return tex_config["tex_body"].replace( + tex_config["text_to_replace"], + new_tex + ) + + def get_modified_expression(self): + tex_components = [] + to_isolate = self.tex_obj.substrings_to_isolate + for tex_substring in self.tex_obj.tex_substrings: + if tex_substring not in to_isolate: + tex_components.append(tex_substring) + else: + color_index = to_isolate.index(tex_substring) + tex_components.append("".join([ + "{\\color[RGB]{", + str(self.get_nth_color_tuple(color_index))[1:-1], + "}", + tex_substring, + "}" + ])) + return self.tex_obj.arg_separator.join(tex_components) + + def break_up_by_substrings(self): + """ + Reorganize existing submojects one layer + deeper based on the structure of tex_substrings (as a list + of tex_substrings) + """ + if len(self.tex_obj.tex_substrings) == 1: + submob = self.copy() + self.set_submobjects([submob]) + return self + new_submobjects = [] + new_submobject_components = [] + for part in self.submobjects: + if part.fill_color is not None: + if new_submobject_components: + new_submobjects.append(VGroup(*new_submobject_components)) + new_submobject_components = [] + new_submobjects.append(part) + else: + new_submobject_components.append(part) + if new_submobject_components: + new_submobjects.append(VGroup(*new_submobject_components)) + + for submob, tex_substring in zip(new_submobjects, self.tex_obj.tex_substrings): + fill_color = submob.fill_color + if fill_color is not None: + submob_tex_string = self.tex_obj.substrings_to_isolate[int(fill_color[1:], 16) - 1] + else: + submob_tex_string = tex_substring + submob.tex_string = submob_tex_string + # Prevent methods and classes using `get_tex()` from breaking. + submob.get_tex = MethodType(lambda sm: sm.tex_string, submob) + self.set_submobjects(new_submobjects) + + return self + + @staticmethod + def get_nth_color_tuple(n): ## TODO: Refactor + # Get a unique color different from black, + # or the svg file will not include the color information. + return ( + (n + 1) // 256 // 256, + (n + 1) // 256 % 256, + (n + 1) % 256 + ) + + +class Tex(VMobject): + CONFIG = { + "fill_opacity": 1.0, + "stroke_width": 0, + "should_center": True, + "font_size": 48, + "height": None, + "organize_left_to_right": False, + "alignment": "\\centering", + "math_mode": True, + "arg_separator": "", + "isolate": [], + "tex_to_color_map": {}, + } + + def __init__(self, *tex_strings, **kwargs): + super().__init__(**kwargs) + self.substrings_to_isolate = [*self.isolate, *self.tex_to_color_map.keys()] + self.tex_substrings = self.break_up_tex_strings(tex_strings) + tex_string = self.arg_separator.join(tex_strings) + self.tex_string = tex_string + + hash_val = self.tex2hash() + if hash_val not in tex_hash_to_mob_map: ## TODO + with display_during_execution(f" Writing \"{tex_string}\""): + svg_mob = SVGTex(self) + tex_hash_to_mob_map[hash_val] = svg_mob + self.add(*( + sm.copy() + for sm in tex_hash_to_mob_map[hash_val] + )) + self.init_colors() + self.set_color_by_tex_to_color_map(self.tex_to_color_map, substring=False) + + 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 tex2hash(self): + id_str = self.tex_string + str(self.substrings_to_isolate) + hasher = hashlib.sha256() + hasher.update(id_str.encode()) + return hasher.hexdigest()[:16] + + def break_up_tex_strings(self, tex_strings): + # Separate out any strings specified in the isolate + # or tex_to_color_map lists. + if len(self.substrings_to_isolate) == 0: + return tex_strings + patterns = ( + "({})".format(re.escape(ss)) + for ss in self.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 organize_submobjects_left_to_right(self): + self.sort(lambda p: p[0]) + 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(*[ + mob for mob in self.submobjects if test(tex, mob.get_tex()) + ]) + + 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 TexText(Tex): + CONFIG = { + "math_mode": False, + "arg_separator": "", + } + + +class BulletedList(TexText): + CONFIG = { + "buff": MED_LARGE_BUFF, + "dot_scale_factor": 2, + "alignment": "", + } + + def __init__(self, *items, **kwargs): + line_separated_items = [s + "\\\\" for s in items] + TexText.__init__(self, *line_separated_items, **kwargs) + for part in self: + dot = Tex("\\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 TexFromPresetString(Tex): + CONFIG = { + # To be filled by subclasses + "tex": None, + "color": None, + } + + def __init__(self, **kwargs): + digest_config(self, kwargs) + Tex.__init__(self, self.tex, **kwargs) + self.set_color(self.color) + + +class Title(TexText): + 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): + TexText.__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 From 7ffab788b79c3bc09b76494604690a4612a99362 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Sat, 27 Nov 2021 16:16:18 +0800 Subject: [PATCH 04/30] Recover `numbers.py` --- manimlib/mobject/numbers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index b4850b12..7a8d05ba 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -1,5 +1,5 @@ from manimlib.constants import * -from manimlib.mobject.svg.tex_mobject import Tex +from manimlib.mobject.svg.tex_mobject import SingleStringTex from manimlib.mobject.svg.text_mobject import Text from manimlib.mobject.types.vectorized_mobject import VMobject @@ -40,7 +40,7 @@ class DecimalNumber(VMobject): dots.arrange(RIGHT, buff=2 * dots[0].get_width()) self.add(dots) if self.unit is not None: - self.unit_sign = self.string_to_mob(self.unit, Tex) + self.unit_sign = self.string_to_mob(self.unit, SingleStringTex) self.add(self.unit_sign) self.arrange( From e3f87d835bea035bcaa507a4bbe70728683e358c Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Sat, 27 Nov 2021 16:17:22 +0800 Subject: [PATCH 05/30] Recover files --- manimlib/mobject/svg/brace.py | 3 +- manimlib/mobject/svg/tex_mobject.py | 300 +++++++++++++++------------- 2 files changed, 162 insertions(+), 141 deletions(-) diff --git a/manimlib/mobject/svg/brace.py b/manimlib/mobject/svg/brace.py index 1096f484..cd686fbb 100644 --- a/manimlib/mobject/svg/brace.py +++ b/manimlib/mobject/svg/brace.py @@ -6,6 +6,7 @@ from manimlib.constants import * from manimlib.animation.fading import FadeIn from manimlib.animation.growing import GrowFromCenter from manimlib.mobject.svg.tex_mobject import Tex +from manimlib.mobject.svg.tex_mobject import SingleStringTex from manimlib.mobject.svg.tex_mobject import TexText from manimlib.mobject.svg.text_mobject import Text from manimlib.mobject.types.vectorized_mobject import VMobject @@ -13,7 +14,7 @@ from manimlib.utils.config_ops import digest_config from manimlib.utils.space_ops import get_norm -class Brace(Tex): +class Brace(SingleStringTex): CONFIG = { "buff": 0.2, "tex_string": r"\underbrace{\qquad}" diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 1489ee1a..6f1054ca 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -1,8 +1,6 @@ from functools import reduce -import hashlib import operator as op import re -from types import MethodType from manimlib.constants import * from manimlib.mobject.geometry import Line @@ -18,119 +16,10 @@ from manimlib.utils.tex_file_writing import display_during_execution SCALE_FACTOR_PER_FONT_POINT = 0.001 -tex_hash_to_mob_map = {} +tex_string_to_mob_map = {} -class SVGTex(SVGMobject): - CONFIG = { - "height": None, - # The hierachy structure is needed for the `break_up_by_substrings` method - "unpack_groups": False, - "path_string_config": { - "should_subdivide_sharp_curves": True, - "should_remove_null_curves": True, - } - } - - def __init__(self, tex_obj, **kwargs): - self.tex_obj = tex_obj - full_tex = self.get_tex_file_body() - filename = tex_to_svg_file(full_tex) - super().__init__(filename, **kwargs) - self.break_up_by_substrings() - self.init_colors() - - def get_mobjects_from(self, element): - result = super().get_mobjects_from(element) - if len(result) == 0: - return result - result[0].fill_color = None - try: - fill_color = element.getAttribute("fill") - if fill_color: - result[0].fill_color = fill_color - except: - pass - return result - - def get_tex_file_body(self): - new_tex = self.get_modified_expression() - if self.tex_obj.math_mode: - new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}" - - new_tex = self.tex_obj.alignment + "\n" + new_tex - - tex_config = get_tex_config() - return tex_config["tex_body"].replace( - tex_config["text_to_replace"], - new_tex - ) - - def get_modified_expression(self): - tex_components = [] - to_isolate = self.tex_obj.substrings_to_isolate - for tex_substring in self.tex_obj.tex_substrings: - if tex_substring not in to_isolate: - tex_components.append(tex_substring) - else: - color_index = to_isolate.index(tex_substring) - tex_components.append("".join([ - "{\\color[RGB]{", - str(self.get_nth_color_tuple(color_index))[1:-1], - "}", - tex_substring, - "}" - ])) - return self.tex_obj.arg_separator.join(tex_components) - - def break_up_by_substrings(self): - """ - Reorganize existing submojects one layer - deeper based on the structure of tex_substrings (as a list - of tex_substrings) - """ - if len(self.tex_obj.tex_substrings) == 1: - submob = self.copy() - self.set_submobjects([submob]) - return self - new_submobjects = [] - new_submobject_components = [] - for part in self.submobjects: - if part.fill_color is not None: - if new_submobject_components: - new_submobjects.append(VGroup(*new_submobject_components)) - new_submobject_components = [] - new_submobjects.append(part) - else: - new_submobject_components.append(part) - if new_submobject_components: - new_submobjects.append(VGroup(*new_submobject_components)) - - for submob, tex_substring in zip(new_submobjects, self.tex_obj.tex_substrings): - fill_color = submob.fill_color - if fill_color is not None: - submob_tex_string = self.tex_obj.substrings_to_isolate[int(fill_color[1:], 16) - 1] - else: - submob_tex_string = tex_substring - submob.tex_string = submob_tex_string - # Prevent methods and classes using `get_tex()` from breaking. - submob.get_tex = MethodType(lambda sm: sm.tex_string, submob) - self.set_submobjects(new_submobjects) - - return self - - @staticmethod - def get_nth_color_tuple(n): ## TODO: Refactor - # Get a unique color different from black, - # or the svg file will not include the color information. - return ( - (n + 1) // 256 // 256, - (n + 1) // 256 % 256, - (n + 1) % 256 - ) - - -class Tex(VMobject): +class SingleStringTex(VMobject): CONFIG = { "fill_opacity": 1.0, "stroke_width": 0, @@ -140,49 +29,155 @@ class Tex(VMobject): "organize_left_to_right": False, "alignment": "\\centering", "math_mode": True, - "arg_separator": "", - "isolate": [], - "tex_to_color_map": {}, } - def __init__(self, *tex_strings, **kwargs): + def __init__(self, tex_string, **kwargs): super().__init__(**kwargs) - self.substrings_to_isolate = [*self.isolate, *self.tex_to_color_map.keys()] - self.tex_substrings = self.break_up_tex_strings(tex_strings) - tex_string = self.arg_separator.join(tex_strings) + assert(isinstance(tex_string, str)) self.tex_string = tex_string - - hash_val = self.tex2hash() - if hash_val not in tex_hash_to_mob_map: ## TODO + if tex_string not in tex_string_to_mob_map: with display_during_execution(f" Writing \"{tex_string}\""): - svg_mob = SVGTex(self) - tex_hash_to_mob_map[hash_val] = svg_mob + full_tex = self.get_tex_file_body(tex_string) + filename = tex_to_svg_file(full_tex) + svg_mob = SVGMobject( + filename, + height=None, + path_string_config={ + "should_subdivide_sharp_curves": True, + "should_remove_null_curves": True, + } + ) + tex_string_to_mob_map[tex_string] = svg_mob self.add(*( sm.copy() - for sm in tex_hash_to_mob_map[hash_val] + for sm in tex_string_to_mob_map[tex_string] )) self.init_colors() - self.set_color_by_tex_to_color_map(self.tex_to_color_map, substring=False) 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 tex2hash(self): - id_str = self.tex_string + str(self.substrings_to_isolate) - hasher = hashlib.sha256() - hasher.update(id_str.encode()) - return hasher.hexdigest()[:16] + 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*}" + + 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 + ) + + def get_modified_expression(self, tex_string): + return self.modify_special_strings(tex_string.strip()) + + 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 Tex resiliant to unmatched braces + """ + num_unclosed_brackets = 0 + for char in tex: + 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): + return self.tex_string + + def organize_submobjects_left_to_right(self): + self.sort(lambda p: p[0]) + return self + + +class Tex(SingleStringTex): + CONFIG = { + "arg_separator": "", + "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) + 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 any strings specified in the isolate # or tex_to_color_map lists. - if len(self.substrings_to_isolate) == 0: + substrings_to_isolate = [*self.isolate, *self.tex_to_color_map.keys()] + if len(substrings_to_isolate) == 0: return tex_strings patterns = ( "({})".format(re.escape(ss)) - for ss in self.substrings_to_isolate + for ss in substrings_to_isolate ) pattern = "|".join(patterns) pieces = [] @@ -193,9 +188,33 @@ class Tex(VMobject): pieces.append(s) return list(filter(lambda s: s, pieces)) - - def organize_submobjects_left_to_right(self): - self.sort(lambda p: p[0]) + 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 = SingleStringTex(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): @@ -208,9 +227,10 @@ class Tex(VMobject): else: return tex1 == tex2 - return VGroup(*[ - mob for mob in self.submobjects if test(tex, mob.get_tex()) - ]) + return VGroup(*filter( + lambda m: isinstance(m, SingleStringTex) 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) From 13a5f6d6ff8ee12a174c9905d063c754ffdfbc48 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Sat, 27 Nov 2021 16:19:01 +0800 Subject: [PATCH 06/30] Add `MTex` --- manimlib/mobject/svg/mtex_mobject.py | 347 +++++++++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 manimlib/mobject/svg/mtex_mobject.py diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py new file mode 100644 index 00000000..3b9f51f2 --- /dev/null +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -0,0 +1,347 @@ +import itertools as it +import re + +from manimlib.constants import * +from manimlib.mobject.svg.svg_mobject import SVGMobject +from manimlib.mobject.types.vectorized_mobject import VMobject +from manimlib.mobject.types.vectorized_mobject import VGroup +from manimlib.utils.iterables import adjacent_pairs +from manimlib.utils.iterables import remove_list_redundancies +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 + + +tex_hash_to_mob_map = {} + + +class _LabelledTex(SVGMobject): + CONFIG = { + "height": None, + "path_string_config": { + "should_subdivide_sharp_curves": True, + "should_remove_null_curves": True, + }, + } + + def get_mobjects_from(self, element): + result = super().get_mobjects_from(element) + for mob in result: + if not hasattr(mob, "label_str"): + mob.label_str = "" + try: + label_str = element.getAttribute("fill") + if label_str: + if len(label_str) == 4: + # "#RGB" => "#RRGGBB" + label_str = "#" + "".join([c * 2 for c in label_str[1:]]) + for mob in result: + mob.label_str = label_str + except: + pass + return result + + +class _TexSpan(object): + def __init__(self, script_type, label, referring_labels): + # 0 for normal, 1 for subscript, 2 for superscript. + self.script_type = script_type + self.label = label + self.referring_labels = referring_labels + + +class MTex(VMobject): + CONFIG = { + "fill_opacity": 1.0, + "stroke_width": 0, + "should_center": True, + "font_size": 48, + "height": None, + "organize_left_to_right": False, + "alignment": "\\centering", + "math_mode": True, + "isolate": [], + "tex_to_color_map": {}, + } + + def __init__(self, tex_string, **kwargs): + super().__init__(**kwargs) + self.tex_string = tex_string + self.parse_tex() + + full_tex = self.get_tex_file_body() + hash_val = hash(full_tex) + if hash_val not in tex_hash_to_mob_map: + with display_during_execution(f"Writing \"{tex_string}\""): + filename = tex_to_svg_file(full_tex) + svg_mob = _LabelledTex(filename) + tex_hash_to_mob_map[hash_val] = svg_mob + self.add(*[ + submob.copy() + for submob in tex_hash_to_mob_map[hash_val] + ]) + self.build_structure() + + self.init_colors() + self.set_color_by_tex_to_color_map(self.tex_to_color_map) + + 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 add_tex_span(self, span_tuple, script_type=0, referring_labels=None): + if referring_labels is None: + # Should be additionally labelled. + label = self.current_label + self.current_label += 1 + referring_labels = [label] + else: + label = -1 + + # 0 for normal, 1 for subscript, 2 for superscript. + # Only those spans with `label != -1` will be colored. + tex_span = _TexSpan(script_type, label, referring_labels) + self.tex_spans_dict[span_tuple] = tex_span + + def parse_tex(self): + self.tex_spans_dict = {} + self.current_label = 0 + self.break_up_by_braces() + self.break_up_by_scripts() + self.break_up_by_additional_strings() + self.analyse_referring_colors() + + def break_up_by_braces(self): + span_tuples = [] + left_brace_indices = [] + for match_obj in re.finditer(r"(?= span_end: + continue + span_tuple = (span_begin, span_end) + if span_tuple not in self.tex_spans_dict: + self.add_tex_span(span_tuple) + + def analyse_referring_colors(self): + all_span_tuples = list(self.tex_spans_dict.keys()) + if not all_span_tuples: + return + + for i, span_0 in enumerate(all_span_tuples): + for j, span_1 in enumerate(all_span_tuples): + if i == j: + continue + tex_span_0 = self.tex_spans_dict[span_0] + tex_span_1 = self.tex_spans_dict[span_1] + if tex_span_0.label == -1: + continue + if span_0[0] <= span_1[0] and span_0[1] >= span_1[1]: + tex_span_0.referring_labels.append(tex_span_1.label) + + def raise_tex_parsing_error(self): + raise ValueError(f"Failed to parse tex: \"{self.tex_string}\"") + + def get_tex_file_body(self): + new_tex = self.get_modified_expression() + 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 + ) + + def get_modified_expression(self): + tex_string = self.tex_string + if not self.tex_spans_dict: + return tex_string + + indices_with_labels = sorted([ + (index, i, tex_span.label) + for span_tuple, tex_span in self.tex_spans_dict.items() + for i, index in enumerate(span_tuple) + if tex_span.label != -1 + ], key=lambda t: (t[0], 1 - t[1])) + # Add one more item to ensure all the substrings are joined. + indices_with_labels.append(( + len(tex_string), 0, -1 + )) + + result = tex_string[: indices_with_labels[0][0]] + for index_0_with_label, index_1_with_label in list(adjacent_pairs(indices_with_labels))[:-1]: + index, flag, label = index_0_with_label + if flag == 0: + color_tuple = MTex.label_to_color_tuple(label) + result += "".join([ + "{{", + "\\color[RGB]", + "{" + ",".join(map(str, color_tuple)) + "}" + ]) + else: + result += "}}" + result += tex_string[index : index_1_with_label[0]] + return result + + @staticmethod + def label_to_color_tuple(n): + # Get a unique color different from black, + # or the svg file will not include the color information. + return ( + (n + 1) // 256 // 256, + (n + 1) // 256 % 256, + (n + 1) % 256 + ) + + @staticmethod + def color_str_to_label(color): + return int(color[1:], 16) - 1 + + def build_structure(self): + # Simply pack together adjacent mobjects with the same label. + new_submobjects = [] + new_submobject_components = [] + current_label_str = "" + for submob in self.submobjects: + if submob.label_str == current_label_str: + new_submobject_components.append(submob) + else: + if new_submobject_components: + new_submobjects.append(VGroup(*new_submobject_components)) + new_submobject_components = [submob] + current_label_str = submob.label_str + if new_submobject_components: + new_submobjects.append(VGroup(*new_submobject_components)) + + self.set_submobjects(new_submobjects) + return self + + def get_parts_by_tex(self, tex): + result = VGroup() + d = dict(self.tex_spans_dict.keys()) + for match_obj in re.finditer(re.escape(tex), self.tex_string): + labels = [] + span_begin, span_end = match_obj.span() + while span_begin < span_end and span_begin in d: + next_span_begin = d[span_begin] + referring_labels = self.tex_spans_dict[(span_begin, next_span_begin)].referring_labels + labels.extend(referring_labels) + span_begin = next_span_begin + if span_begin != span_end: + raise ValueError(f"Failed to get span of tex: \"{tex}\"") + + mob = VGroup(*filter( + lambda submob: submob.label_str and MTex.color_str_to_label(submob.label_str) in labels, + it.chain(*self.submobjects) + )) + result.add(mob) + return result + + def get_part_by_tex(self, tex): + all_parts = self.get_parts_by_tex(tex) + return all_parts[0] if all_parts else None + + def set_color_by_tex(self, tex, color): + self.get_parts_by_tex(tex).set_color(color) + return self + + def set_color_by_tex_to_color_map(self, tex_to_color_map): + for tex, color in list(tex_to_color_map.items()): + self.set_color_by_tex(tex, color) + return self + + def index_of_part(self, submob, start=0): + return self.submobjects.index(submob, start) + + def index_of_part_by_tex(self, tex, start=0): + part = self.get_part_by_tex(tex) + return self.index_of_part(part, start) + + def slice_by_tex(self, start_tex=None, stop_tex=None): + if start_tex is None: + start_index = 0 + else: + start_index = self.index_of_part_by_tex(start_tex) + + if stop_tex is None: + return self[start_index:] + else: + stop_index = self.index_of_part_by_tex(stop_tex, start=start_index) + return self[start_index:stop_index] + + def set_bstroke(self, color=BLACK, width=4): + self.set_stroke(color, width, background=True) + return self + + +class MTexText(MTex): + CONFIG = { + "math_mode": False, + } From b1d869cd117ca1c2ac889f6c8e856736a6a1d301 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Sat, 27 Nov 2021 16:21:06 +0800 Subject: [PATCH 07/30] Update `__init__.py` to include `mtex_mobject` --- manimlib/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/__init__.py b/manimlib/__init__.py index 4dd188c6..0a45a9c0 100644 --- a/manimlib/__init__.py +++ b/manimlib/__init__.py @@ -37,6 +37,7 @@ from manimlib.mobject.probability import * from manimlib.mobject.shape_matchers import * from manimlib.mobject.svg.brace import * from manimlib.mobject.svg.drawings import * +from manimlib.mobject.svg.mtex_mobject import * from manimlib.mobject.svg.svg_mobject import * from manimlib.mobject.svg.tex_mobject import * from manimlib.mobject.svg.text_mobject import * From c84acc0023ad24f28ebbbc1ef296ee3caaa1e4b9 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Sat, 27 Nov 2021 19:53:52 +0800 Subject: [PATCH 08/30] Remove disabled methods --- manimlib/mobject/svg/mtex_mobject.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 3b9f51f2..3973b30a 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -1,7 +1,6 @@ import itertools as it import re -from manimlib.constants import * from manimlib.mobject.svg.svg_mobject import SVGMobject from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.mobject.types.vectorized_mobject import VGroup @@ -317,29 +316,6 @@ class MTex(VMobject): self.set_color_by_tex(tex, color) return self - def index_of_part(self, submob, start=0): - return self.submobjects.index(submob, start) - - def index_of_part_by_tex(self, tex, start=0): - part = self.get_part_by_tex(tex) - return self.index_of_part(part, start) - - def slice_by_tex(self, start_tex=None, stop_tex=None): - if start_tex is None: - start_index = 0 - else: - start_index = self.index_of_part_by_tex(start_tex) - - if stop_tex is None: - return self[start_index:] - else: - stop_index = self.index_of_part_by_tex(stop_tex, start=start_index) - return self[start_index:stop_index] - - def set_bstroke(self, color=BLACK, width=4): - self.set_stroke(color, width, background=True) - return self - class MTexText(MTex): CONFIG = { From 697028cd4c8a9db94c77f2e330c9e0338a663594 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Sat, 27 Nov 2021 23:07:46 +0800 Subject: [PATCH 09/30] Add slicing and indexing methods --- manimlib/mobject/svg/mtex_mobject.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 3973b30a..3da55680 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -303,9 +303,9 @@ class MTex(VMobject): result.add(mob) return result - def get_part_by_tex(self, tex): + def get_part_by_tex(self, tex, index=0): all_parts = self.get_parts_by_tex(tex) - return all_parts[0] if all_parts else None + return all_parts[index] def set_color_by_tex(self, tex, color): self.get_parts_by_tex(tex).set_color(color) @@ -316,6 +316,29 @@ class MTex(VMobject): self.set_color_by_tex(tex, color) return self + def slice_of_part(self, part): + # Only finds where the head and the tail of `part` is in. + submobs = self.submobjects + begin_mob = part[0] + begin_index = 0 + while begin_mob not in submobs[begin_index]: + begin_index += 1 + end_mob = part[-1] + end_index = len(submobs) - 1 + while end_mob not in submobs[end_index]: + end_index -= 1 + return slice(begin_index, end_index + 1) + + def slice_of_part_by_tex(self, tex, index=0): + part = self.get_part_by_tex(tex, index=index) + return self.slice_of_part(part) + + def index_of_part(self, part): + return self.slice_of_part(part).start + + def index_of_part_by_tex(self, tex, index=0): + return self.slice_of_part_by_tex(tex, index=index).start + class MTexText(MTex): CONFIG = { From a4f9de1ca173e7a5b8b343445eb5d8e2425fa387 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Sun, 28 Nov 2021 12:14:29 +0800 Subject: [PATCH 10/30] Fix bugs concerned with coloring --- manimlib/mobject/svg/mtex_mobject.py | 78 ++++++++++++++++++---------- 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 3da55680..8f900d88 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -45,11 +45,17 @@ class _LabelledTex(SVGMobject): class _TexSpan(object): - def __init__(self, script_type, label, referring_labels): + def __init__(self, script_type, label, containing_labels): # 0 for normal, 1 for subscript, 2 for superscript. self.script_type = script_type self.label = label - self.referring_labels = referring_labels + self.containing_labels = containing_labels + + def __repr__(self): + return "_TexSpan(" + ", ".join([ + attrib_name + "=" + str(getattr(self, attrib_name)) + for attrib_name in ["script_type", "label", "containing_labels"] + ]) + ")" class MTex(VMobject): @@ -92,18 +98,18 @@ class MTex(VMobject): if self.organize_left_to_right: self.organize_submobjects_left_to_right() - def add_tex_span(self, span_tuple, script_type=0, referring_labels=None): - if referring_labels is None: + def add_tex_span(self, span_tuple, script_type=0, containing_labels=None): + if containing_labels is None: # Should be additionally labelled. label = self.current_label self.current_label += 1 - referring_labels = [label] + containing_labels = [label] else: label = -1 # 0 for normal, 1 for subscript, 2 for superscript. # Only those spans with `label != -1` will be colored. - tex_span = _TexSpan(script_type, label, referring_labels) + tex_span = _TexSpan(script_type, label, containing_labels) self.tex_spans_dict[span_tuple] = tex_span def parse_tex(self): @@ -112,7 +118,7 @@ class MTex(VMobject): self.break_up_by_braces() self.break_up_by_scripts() self.break_up_by_additional_strings() - self.analyse_referring_colors() + self.analyse_containing_labels() def break_up_by_braces(self): span_tuples = [] @@ -154,7 +160,7 @@ class MTex(VMobject): self.add_tex_span( (token_begin, content_span[1]), script_type=script_type, - referring_labels=[label] + containing_labels=[label] ) def break_up_by_additional_strings(self): @@ -186,7 +192,7 @@ class MTex(VMobject): if span_tuple not in self.tex_spans_dict: self.add_tex_span(span_tuple) - def analyse_referring_colors(self): + def analyse_containing_labels(self): all_span_tuples = list(self.tex_spans_dict.keys()) if not all_span_tuples: return @@ -200,7 +206,7 @@ class MTex(VMobject): if tex_span_0.label == -1: continue if span_0[0] <= span_1[0] and span_0[1] >= span_1[1]: - tex_span_0.referring_labels.append(tex_span_1.label) + tex_span_0.containing_labels.append(tex_span_1.label) def raise_tex_parsing_error(self): raise ValueError(f"Failed to parse tex: \"{self.tex_string}\"") @@ -224,19 +230,19 @@ class MTex(VMobject): return tex_string indices_with_labels = sorted([ - (index, i, tex_span.label) + (span_tuple[i], i, span_tuple[1 - i], tex_span.label) for span_tuple, tex_span in self.tex_spans_dict.items() - for i, index in enumerate(span_tuple) + for i in range(2) if tex_span.label != -1 - ], key=lambda t: (t[0], 1 - t[1])) + ], key=lambda t: (t[0], 1 - t[1], -t[2])) # Add one more item to ensure all the substrings are joined. indices_with_labels.append(( - len(tex_string), 0, -1 + len(tex_string), 0, 0, -1 )) result = tex_string[: indices_with_labels[0][0]] for index_0_with_label, index_1_with_label in list(adjacent_pairs(indices_with_labels))[:-1]: - index, flag, label = index_0_with_label + index, flag, _, label = index_0_with_label if flag == 0: color_tuple = MTex.label_to_color_tuple(label) result += "".join([ @@ -283,21 +289,37 @@ class MTex(VMobject): return self def get_parts_by_tex(self, tex): - result = VGroup() - d = dict(self.tex_spans_dict.keys()) - for match_obj in re.finditer(re.escape(tex), self.tex_string): - labels = [] - span_begin, span_end = match_obj.span() - while span_begin < span_end and span_begin in d: - next_span_begin = d[span_begin] - referring_labels = self.tex_spans_dict[(span_begin, next_span_begin)].referring_labels - labels.extend(referring_labels) - span_begin = next_span_begin - if span_begin != span_end: - raise ValueError(f"Failed to get span of tex: \"{tex}\"") + all_span_tuples = sorted( + list(self.tex_spans_dict.keys()), + key=lambda t: (t[0], -t[1]) + ) + def find_components_of_span(span_tuple, partial_components=[]): + span_begin, span_end = span_tuple + if span_begin == span_end: + return partial_components + if span_begin > span_end: + return None + next_tuple_choices = filter(lambda t: t[0] == span_begin, all_span_tuples) + for possible_tuple in next_tuple_choices: + result = find_components_of_span( + (possible_tuple[1], span_end), [*partial_components, possible_tuple] + ) + if result is not None: + return result + return None + result = VGroup() + for match_obj in re.finditer(re.escape(tex), self.tex_string): + span_tuples = find_components_of_span(match_obj.span()) + if span_tuples is None: + raise ValueError(f"Failed to get span of tex: \"{tex}\"") + labels = remove_list_redundancies(list(it.chain(*[ + self.tex_spans_dict[span_tuple].containing_labels + for span_tuple in span_tuples + ]))) mob = VGroup(*filter( - lambda submob: submob.label_str and MTex.color_str_to_label(submob.label_str) in labels, + lambda submob: submob.label_str \ + and MTex.color_str_to_label(submob.label_str) in labels, it.chain(*self.submobjects) )) result.add(mob) From dae24891fab10f993ef0b42c63737e53b9ce47ae Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Sun, 28 Nov 2021 13:03:33 +0800 Subject: [PATCH 11/30] Add `get_all_isolated_substrings` method --- manimlib/mobject/svg/mtex_mobject.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 8f900d88..7e03fdd5 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -288,6 +288,13 @@ class MTex(VMobject): self.set_submobjects(new_submobjects) return self + def get_all_isolated_substrings(self): + tex_string = self.tex_string + return remove_list_redundancies([ + tex_string[span_tuple[0] : span_tuple[1]] + for span_tuple in self.tex_spans_dict.keys() + ]) + def get_parts_by_tex(self, tex): all_span_tuples = sorted( list(self.tex_spans_dict.keys()), From e8ebfa312b0d594d535fbcba1587f932363ac3b5 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Sun, 28 Nov 2021 13:26:54 +0800 Subject: [PATCH 12/30] Prevent infinite loops from unexpected inputs --- manimlib/mobject/svg/mtex_mobject.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 7e03fdd5..ca0c4ca1 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -167,6 +167,8 @@ class MTex(VMobject): additional_strings_to_break_up = remove_list_redundancies([ *self.isolate, *self.tex_to_color_map.keys() ]) + if "" in additional_strings_to_break_up: + additional_strings_to_break_up.remove("") if not additional_strings_to_break_up: return @@ -291,7 +293,7 @@ class MTex(VMobject): def get_all_isolated_substrings(self): tex_string = self.tex_string return remove_list_redundancies([ - tex_string[span_tuple[0] : span_tuple[1]] + tex_string[slice(*span_tuple)] for span_tuple in self.tex_spans_dict.keys() ]) @@ -348,14 +350,17 @@ class MTex(VMobject): def slice_of_part(self, part): # Only finds where the head and the tail of `part` is in. submobs = self.submobjects + submobs_len = len(submobs) begin_mob = part[0] begin_index = 0 - while begin_mob not in submobs[begin_index]: + while begin_mob < submobs_len and begin_mob not in submobs[begin_index]: begin_index += 1 end_mob = part[-1] - end_index = len(submobs) - 1 - while end_mob not in submobs[end_index]: + end_index = submobs_len - 1 + while end_index >= 0 and end_mob not in submobs[end_index]: end_index -= 1 + if begin_index > end_index: + raise ValueError("Unable to find part") return slice(begin_index, end_index + 1) def slice_of_part_by_tex(self, tex, index=0): From 758f2ec2360eefe315630cb3f72bd00983914cbe Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Sun, 28 Nov 2021 23:38:12 +0800 Subject: [PATCH 13/30] Some small refactor --- manimlib/mobject/svg/mtex_mobject.py | 36 ++++++++++------------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index ca0c4ca1..c428d487 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -45,11 +45,11 @@ class _LabelledTex(SVGMobject): class _TexSpan(object): - def __init__(self, script_type, label, containing_labels): + def __init__(self, script_type, label): # 0 for normal, 1 for subscript, 2 for superscript. self.script_type = script_type self.label = label - self.containing_labels = containing_labels + self.containing_labels = [] def __repr__(self): return "_TexSpan(" + ", ".join([ @@ -98,18 +98,17 @@ class MTex(VMobject): if self.organize_left_to_right: self.organize_submobjects_left_to_right() - def add_tex_span(self, span_tuple, script_type=0, containing_labels=None): - if containing_labels is None: + def add_tex_span(self, span_tuple, script_type=0): + if script_type == 0: # Should be additionally labelled. label = self.current_label self.current_label += 1 - containing_labels = [label] else: label = -1 # 0 for normal, 1 for subscript, 2 for superscript. # Only those spans with `label != -1` will be colored. - tex_span = _TexSpan(script_type, label, containing_labels) + tex_span = _TexSpan(script_type, label) self.tex_spans_dict[span_tuple] = tex_span def parse_tex(self): @@ -159,8 +158,7 @@ class MTex(VMobject): label = self.tex_spans_dict[content_span].label self.add_tex_span( (token_begin, content_span[1]), - script_type=script_type, - containing_labels=[label] + script_type=script_type ) def break_up_by_additional_strings(self): @@ -196,18 +194,11 @@ class MTex(VMobject): def analyse_containing_labels(self): all_span_tuples = list(self.tex_spans_dict.keys()) - if not all_span_tuples: - return - - for i, span_0 in enumerate(all_span_tuples): - for j, span_1 in enumerate(all_span_tuples): - if i == j: - continue + for span_0 in all_span_tuples: + for span_1 in all_span_tuples: tex_span_0 = self.tex_spans_dict[span_0] tex_span_1 = self.tex_spans_dict[span_1] - if tex_span_0.label == -1: - continue - if span_0[0] <= span_1[0] and span_0[1] >= span_1[1]: + if span_0[0] <= span_1[0] and span_0[1] >= span_1[1] and tex_span_1.label != -1: tex_span_0.containing_labels.append(tex_span_1.label) def raise_tex_parsing_error(self): @@ -261,11 +252,10 @@ class MTex(VMobject): def label_to_color_tuple(n): # Get a unique color different from black, # or the svg file will not include the color information. - return ( - (n + 1) // 256 // 256, - (n + 1) // 256 % 256, - (n + 1) % 256 - ) + rgb = n + 1 + rg, b = divmod(rgb, 256) + r, g = divmod(rg, 256) + return r, g, b @staticmethod def color_str_to_label(color): From 83c70a59d849b3cfc410fca61e2658d1c3f7279a Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Mon, 29 Nov 2021 01:15:38 +0800 Subject: [PATCH 14/30] Sort superscripts and subscripts in submobjects --- manimlib/mobject/svg/mtex_mobject.py | 95 ++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 26 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index c428d487..80034736 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -88,7 +88,8 @@ class MTex(VMobject): submob.copy() for submob in tex_hash_to_mob_map[hash_val] ]) - self.build_structure() + self.build_submobjects() + self.sort_scripts_in_tex_order() self.init_colors() self.set_color_by_tex_to_color_map(self.tex_to_color_map) @@ -193,13 +194,12 @@ class MTex(VMobject): self.add_tex_span(span_tuple) def analyse_containing_labels(self): - all_span_tuples = list(self.tex_spans_dict.keys()) - for span_0 in all_span_tuples: - for span_1 in all_span_tuples: - tex_span_0 = self.tex_spans_dict[span_0] - tex_span_1 = self.tex_spans_dict[span_1] - if span_0[0] <= span_1[0] and span_0[1] >= span_1[1] and tex_span_1.label != -1: - tex_span_0.containing_labels.append(tex_span_1.label) + for span_0, tex_span_0 in self.tex_spans_dict.items(): + if tex_span_0.label == -1: + continue + for span_1, tex_span_1 in self.tex_spans_dict.items(): + if span_1[0] <= span_0[0] and span_0[1] <= span_1[1]: + tex_span_1.containing_labels.append(tex_span_0.label) def raise_tex_parsing_error(self): raise ValueError(f"Failed to parse tex: \"{self.tex_string}\"") @@ -227,25 +227,29 @@ class MTex(VMobject): for span_tuple, tex_span in self.tex_spans_dict.items() for i in range(2) if tex_span.label != -1 - ], key=lambda t: (t[0], 1 - t[1], -t[2])) + ], key=lambda t: (t[0], -t[1], -t[2])) # Add one more item to ensure all the substrings are joined. indices_with_labels.append(( len(tex_string), 0, 0, -1 )) result = tex_string[: indices_with_labels[0][0]] - for index_0_with_label, index_1_with_label in list(adjacent_pairs(indices_with_labels))[:-1]: - index, flag, _, label = index_0_with_label + index_with_label_pairs = list(adjacent_pairs(indices_with_labels))[:-1] + for index_with_label, next_index_with_label in index_with_label_pairs: + index, flag, _, label = index_with_label + # Adding one more pair of braces will help maintain the glyghs of tex file... if flag == 0: color_tuple = MTex.label_to_color_tuple(label) result += "".join([ "{{", "\\color[RGB]", - "{" + ",".join(map(str, color_tuple)) + "}" + "{", + ",".join(map(str, color_tuple)), + "}" ]) else: result += "}}" - result += tex_string[index : index_1_with_label[0]] + result += tex_string[index : next_index_with_label[0]] return result @staticmethod @@ -261,7 +265,7 @@ class MTex(VMobject): def color_str_to_label(color): return int(color[1:], 16) - 1 - def build_structure(self): + def build_submobjects(self): # Simply pack together adjacent mobjects with the same label. new_submobjects = [] new_submobject_components = [] @@ -280,12 +284,48 @@ class MTex(VMobject): self.set_submobjects(new_submobjects) return self - def get_all_isolated_substrings(self): - tex_string = self.tex_string - return remove_list_redundancies([ - tex_string[slice(*span_tuple)] - for span_tuple in self.tex_spans_dict.keys() + def sort_scripts_in_tex_order(self): + # LaTeX always puts superscripts before subscripts. + # This function sorts the submobjects of scripts in the order of tex given. + script_spans_with_types = sorted([ + (index, span_tuple, tex_span.script_type) + for span_tuple, tex_span in self.tex_spans_dict.items() + if tex_span.script_type != 0 + for index in span_tuple ]) + script_span_with_type_pair = list(adjacent_pairs(script_spans_with_types))[:-1] + for span_with_type, next_span_with_type in script_span_with_type_pair: + index_0, span_tuple_0, script_type_0 = span_with_type + index_1, span_tuple_1, script_type_1 = next_span_with_type + if index_0 != index_1: + continue + if script_type_0 == 2 and script_type_1 == 1: + continue + submobs_slice_0 = self.get_slice_by_labels( + self.tex_spans_dict[span_tuple_0].containing_labels + ) + submobs_slice_1 = self.get_slice_by_labels( + self.tex_spans_dict[span_tuple_1].containing_labels + ) + submobs = self.submobjects + self.set_submobjects([ + *submobs[: submobs_slice_1.start], + *submobs[submobs_slice_0], + *submobs[submobs_slice_1.stop : submobs_slice_0.start], + *submobs[submobs_slice_1], + *submobs[submobs_slice_0.stop :] + ]) + + def get_part_by_labels(self, labels): + return VGroup(*filter( + lambda submob: submob.label_str \ + and MTex.color_str_to_label(submob.label_str) in labels, + it.chain(*self.submobjects) + )) + + def get_slice_by_labels(self, labels): + part = self.get_part_by_labels(labels) + return self.slice_of_part(part) def get_parts_by_tex(self, tex): all_span_tuples = sorted( @@ -316,12 +356,8 @@ class MTex(VMobject): self.tex_spans_dict[span_tuple].containing_labels for span_tuple in span_tuples ]))) - mob = VGroup(*filter( - lambda submob: submob.label_str \ - and MTex.color_str_to_label(submob.label_str) in labels, - it.chain(*self.submobjects) - )) - result.add(mob) + part = self.get_part_by_labels(labels) + result.add(part) return result def get_part_by_tex(self, tex, index=0): @@ -343,7 +379,7 @@ class MTex(VMobject): submobs_len = len(submobs) begin_mob = part[0] begin_index = 0 - while begin_mob < submobs_len and begin_mob not in submobs[begin_index]: + while begin_index < submobs_len and begin_mob not in submobs[begin_index]: begin_index += 1 end_mob = part[-1] end_index = submobs_len - 1 @@ -363,6 +399,13 @@ class MTex(VMobject): def index_of_part_by_tex(self, tex, index=0): return self.slice_of_part_by_tex(tex, index=index).start + def get_all_isolated_substrings(self): + tex_string = self.tex_string + return remove_list_redundancies([ + tex_string[slice(*span_tuple)] + for span_tuple in self.tex_spans_dict.keys() + ]) + class MTexText(MTex): CONFIG = { From 1aec0462ec61744644f52c1222dde93768df5b20 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Mon, 29 Nov 2021 01:43:48 +0800 Subject: [PATCH 15/30] Some small refactors --- manimlib/mobject/svg/mtex_mobject.py | 47 ++++++++++++++-------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 80034736..15fc2b89 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -46,7 +46,8 @@ class _LabelledTex(SVGMobject): class _TexSpan(object): def __init__(self, script_type, label): - # 0 for normal, 1 for subscript, 2 for superscript. + # script_type: 0 for normal, 1 for subscript, 2 for superscript. + # Only those spans with `label != -1` will be colored. self.script_type = script_type self.label = label self.containing_labels = [] @@ -107,8 +108,6 @@ class MTex(VMobject): else: label = -1 - # 0 for normal, 1 for subscript, 2 for superscript. - # Only those spans with `label != -1` will be colored. tex_span = _TexSpan(script_type, label) self.tex_spans_dict[span_tuple] = tex_span @@ -301,11 +300,11 @@ class MTex(VMobject): continue if script_type_0 == 2 and script_type_1 == 1: continue - submobs_slice_0 = self.get_slice_by_labels( - self.tex_spans_dict[span_tuple_0].containing_labels + submobs_slice_0 = self.slice_of_part( + self.get_part_by_span_tuples([span_tuple_0]) ) - submobs_slice_1 = self.get_slice_by_labels( - self.tex_spans_dict[span_tuple_1].containing_labels + submobs_slice_1 = self.slice_of_part( + self.get_part_by_span_tuples([span_tuple_1]) ) submobs = self.submobjects self.set_submobjects([ @@ -316,18 +315,18 @@ class MTex(VMobject): *submobs[submobs_slice_0.stop :] ]) - def get_part_by_labels(self, labels): + def get_part_by_span_tuples(self, span_tuples): + labels = remove_list_redundancies(list(it.chain(*[ + self.tex_spans_dict[span_tuple].containing_labels + for span_tuple in span_tuples + ]))) return VGroup(*filter( lambda submob: submob.label_str \ and MTex.color_str_to_label(submob.label_str) in labels, it.chain(*self.submobjects) )) - def get_slice_by_labels(self, labels): - part = self.get_part_by_labels(labels) - return self.slice_of_part(part) - - def get_parts_by_tex(self, tex): + def get_part_by_custom_span_tuple(self, custom_span_tuple): all_span_tuples = sorted( list(self.tex_spans_dict.keys()), key=lambda t: (t[0], -t[1]) @@ -338,7 +337,9 @@ class MTex(VMobject): return partial_components if span_begin > span_end: return None - next_tuple_choices = filter(lambda t: t[0] == span_begin, all_span_tuples) + next_tuple_choices = sorted(list(filter( + lambda t: t[0] == span_begin, all_span_tuples + )), key=lambda t: -t[1]) for possible_tuple in next_tuple_choices: result = find_components_of_span( (possible_tuple[1], span_end), [*partial_components, possible_tuple] @@ -347,16 +348,16 @@ class MTex(VMobject): return result return None + span_tuples = find_components_of_span(custom_span_tuple) + if span_tuples is None: + tex = self.tex_string[slice(*custom_span_tuple)] + raise ValueError(f"Failed to get span of tex: \"{tex}\"") + return self.get_part_by_span_tuples(span_tuples) + + def get_parts_by_tex(self, tex): result = VGroup() for match_obj in re.finditer(re.escape(tex), self.tex_string): - span_tuples = find_components_of_span(match_obj.span()) - if span_tuples is None: - raise ValueError(f"Failed to get span of tex: \"{tex}\"") - labels = remove_list_redundancies(list(it.chain(*[ - self.tex_spans_dict[span_tuple].containing_labels - for span_tuple in span_tuples - ]))) - part = self.get_part_by_labels(labels) + part = self.get_part_by_custom_span_tuple(match_obj.span()) result.add(part) return result @@ -374,7 +375,7 @@ class MTex(VMobject): return self def slice_of_part(self, part): - # Only finds where the head and the tail of `part` is in. + # Only finds where the head and the tail of `part` are. submobs = self.submobjects submobs_len = len(submobs) begin_mob = part[0] From 2501fac32f5606d6eaf30a4556efd78c5726caff Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Mon, 29 Nov 2021 09:38:48 +0800 Subject: [PATCH 16/30] Some small refactors --- manimlib/mobject/svg/mtex_mobject.py | 112 +++++++++++++-------------- 1 file changed, 54 insertions(+), 58 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 15fc2b89..f3c0aa32 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -224,13 +224,11 @@ class MTex(VMobject): indices_with_labels = sorted([ (span_tuple[i], i, span_tuple[1 - i], tex_span.label) for span_tuple, tex_span in self.tex_spans_dict.items() - for i in range(2) if tex_span.label != -1 + for i in range(2) ], key=lambda t: (t[0], -t[1], -t[2])) # Add one more item to ensure all the substrings are joined. - indices_with_labels.append(( - len(tex_string), 0, 0, -1 - )) + indices_with_labels.append((len(tex_string), 0, 0, -1)) result = tex_string[: indices_with_labels[0][0]] index_with_label_pairs = list(adjacent_pairs(indices_with_labels))[:-1] @@ -267,19 +265,18 @@ class MTex(VMobject): def build_submobjects(self): # Simply pack together adjacent mobjects with the same label. new_submobjects = [] - new_submobject_components = [] + new_glyphs = [] current_label_str = "" for submob in self.submobjects: if submob.label_str == current_label_str: - new_submobject_components.append(submob) + new_glyphs.append(submob) else: - if new_submobject_components: - new_submobjects.append(VGroup(*new_submobject_components)) - new_submobject_components = [submob] + if new_glyphs: + new_submobjects.append(VGroup(*new_glyphs)) + new_glyphs = [submob] current_label_str = submob.label_str - if new_submobject_components: - new_submobjects.append(VGroup(*new_submobject_components)) - + if new_glyphs: + new_submobjects.append(VGroup(*new_glyphs)) self.set_submobjects(new_submobjects) return self @@ -293,26 +290,26 @@ class MTex(VMobject): for index in span_tuple ]) script_span_with_type_pair = list(adjacent_pairs(script_spans_with_types))[:-1] - for span_with_type, next_span_with_type in script_span_with_type_pair: - index_0, span_tuple_0, script_type_0 = span_with_type - index_1, span_tuple_1, script_type_1 = next_span_with_type + for span_with_type_0, span_with_type_1 in script_span_with_type_pair: + index_0, span_tuple_0, script_type_0 = span_with_type_0 + index_1, span_tuple_1, script_type_1 = span_with_type_1 if index_0 != index_1: continue if script_type_0 == 2 and script_type_1 == 1: continue - submobs_slice_0 = self.slice_of_part( + submob_slice_0 = self.slice_of_part( self.get_part_by_span_tuples([span_tuple_0]) ) - submobs_slice_1 = self.slice_of_part( + submob_slice_1 = self.slice_of_part( self.get_part_by_span_tuples([span_tuple_1]) ) submobs = self.submobjects self.set_submobjects([ - *submobs[: submobs_slice_1.start], - *submobs[submobs_slice_0], - *submobs[submobs_slice_1.stop : submobs_slice_0.start], - *submobs[submobs_slice_1], - *submobs[submobs_slice_0.stop :] + *submobs[: submob_slice_1.start], + *submobs[submob_slice_0], + *submobs[submob_slice_1.stop : submob_slice_0.start], + *submobs[submob_slice_1], + *submobs[submob_slice_0.stop :] ]) def get_part_by_span_tuples(self, span_tuples): @@ -326,40 +323,37 @@ class MTex(VMobject): it.chain(*self.submobjects) )) - def get_part_by_custom_span_tuple(self, custom_span_tuple): - all_span_tuples = sorted( - list(self.tex_spans_dict.keys()), - key=lambda t: (t[0], -t[1]) - ) - def find_components_of_span(span_tuple, partial_components=[]): - span_begin, span_end = span_tuple - if span_begin == span_end: - return partial_components - if span_begin > span_end: - return None - next_tuple_choices = sorted(list(filter( - lambda t: t[0] == span_begin, all_span_tuples - )), key=lambda t: -t[1]) - for possible_tuple in next_tuple_choices: - result = find_components_of_span( - (possible_tuple[1], span_end), [*partial_components, possible_tuple] - ) - if result is not None: - return result + def find_components_of_custom_span(self, custom_span_tuple, partial_components=[]): + span_begin, span_end = custom_span_tuple + if span_begin == span_end: + return partial_components + next_begin_choices = sorted([ + span_tuple[1] + for span_tuple in self.tex_spans_dict.keys() + if span_tuple[0] == span_begin and span_tuple[1] <= span_end + ], reverse=True) + if not next_begin_choices: return None + for next_begin in next_begin_choices: + result = self.find_components_of_custom_span( + (next_begin, span_end), [*partial_components, (span_begin, next_begin)] + ) + if result is not None: + return result + return None - span_tuples = find_components_of_span(custom_span_tuple) + def get_part_by_custom_span_tuple(self, custom_span_tuple): + span_tuples = self.find_components_of_custom_span(custom_span_tuple) if span_tuples is None: tex = self.tex_string[slice(*custom_span_tuple)] raise ValueError(f"Failed to get span of tex: \"{tex}\"") return self.get_part_by_span_tuples(span_tuples) def get_parts_by_tex(self, tex): - result = VGroup() - for match_obj in re.finditer(re.escape(tex), self.tex_string): - part = self.get_part_by_custom_span_tuple(match_obj.span()) - result.add(part) - return result + return VGroup(*[ + self.get_part_by_custom_span_tuple(match_obj.span()) + for match_obj in re.finditer(re.escape(tex), self.tex_string) + ]) def get_part_by_tex(self, tex, index=0): all_parts = self.get_parts_by_tex(tex) @@ -374,18 +368,20 @@ class MTex(VMobject): self.set_color_by_tex(tex, color) return self - def slice_of_part(self, part): - # Only finds where the head and the tail of `part` are. + def index_of_glyph(self, glyph): submobs = self.submobjects submobs_len = len(submobs) - begin_mob = part[0] - begin_index = 0 - while begin_index < submobs_len and begin_mob not in submobs[begin_index]: - begin_index += 1 - end_mob = part[-1] - end_index = submobs_len - 1 - while end_index >= 0 and end_mob not in submobs[end_index]: - end_index -= 1 + result = 0 + while result < submobs_len and glyph not in submobs[result]: + result += 1 + if result == submobs_len: + raise ValueError("Unable to find mob in tex") + return result + + def slice_of_part(self, part): + # Only finds where the head and the tail of `part` are. + begin_index = self.index_of_glyph(part[0]) + end_index = self.index_of_glyph(part[-1]) if begin_index > end_index: raise ValueError("Unable to find part") return slice(begin_index, end_index + 1) From 8b1715379d658803996af141e81b19ecdf0bf731 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Mon, 29 Nov 2021 09:48:00 +0800 Subject: [PATCH 17/30] Some small refactors --- manimlib/mobject/svg/mtex_mobject.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index f3c0aa32..0bd0c3f1 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -311,6 +311,7 @@ class MTex(VMobject): *submobs[submob_slice_1], *submobs[submob_slice_0.stop :] ]) + return self def get_part_by_span_tuples(self, span_tuples): labels = remove_list_redundancies(list(it.chain(*[ @@ -323,27 +324,25 @@ class MTex(VMobject): it.chain(*self.submobjects) )) - def find_components_of_custom_span(self, custom_span_tuple, partial_components=[]): + def find_span_components_of_custom_span(self, custom_span_tuple, partial_result=[]): span_begin, span_end = custom_span_tuple if span_begin == span_end: - return partial_components + return partial_result next_begin_choices = sorted([ span_tuple[1] for span_tuple in self.tex_spans_dict.keys() if span_tuple[0] == span_begin and span_tuple[1] <= span_end ], reverse=True) - if not next_begin_choices: - return None for next_begin in next_begin_choices: - result = self.find_components_of_custom_span( - (next_begin, span_end), [*partial_components, (span_begin, next_begin)] + result = self.find_span_components_of_custom_span( + (next_begin, span_end), [*partial_result, (span_begin, next_begin)] ) if result is not None: return result return None def get_part_by_custom_span_tuple(self, custom_span_tuple): - span_tuples = self.find_components_of_custom_span(custom_span_tuple) + span_tuples = self.find_span_components_of_custom_span(custom_span_tuple) if span_tuples is None: tex = self.tex_string[slice(*custom_span_tuple)] raise ValueError(f"Failed to get span of tex: \"{tex}\"") @@ -375,7 +374,7 @@ class MTex(VMobject): while result < submobs_len and glyph not in submobs[result]: result += 1 if result == submobs_len: - raise ValueError("Unable to find mob in tex") + raise ValueError("Unable to find glyph in tex") return result def slice_of_part(self, part): @@ -383,7 +382,7 @@ class MTex(VMobject): begin_index = self.index_of_glyph(part[0]) end_index = self.index_of_glyph(part[-1]) if begin_index > end_index: - raise ValueError("Unable to find part") + raise ValueError("Unable to find part in tex") return slice(begin_index, end_index + 1) def slice_of_part_by_tex(self, tex, index=0): From 950466c1da35b73ad6a6d847dbc25f652b48d1ea Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Sun, 5 Dec 2021 10:21:55 +0800 Subject: [PATCH 18/30] Some refactors --- manimlib/mobject/svg/mtex_mobject.py | 99 +++++++++++++++------------- 1 file changed, 52 insertions(+), 47 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 0bd0c3f1..625f7f2f 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -26,19 +26,24 @@ class _LabelledTex(SVGMobject): }, } + @staticmethod + def color_str_to_label(color): + return int(color[1:], 16) - 1 + def get_mobjects_from(self, element): result = super().get_mobjects_from(element) for mob in result: - if not hasattr(mob, "label_str"): - mob.label_str = "" + if not hasattr(mob, "glyph_label"): + mob.glyph_label = -1 try: - label_str = element.getAttribute("fill") - if label_str: - if len(label_str) == 4: + color_str = element.getAttribute("fill") + if color_str: + if len(color_str) == 4: # "#RGB" => "#RRGGBB" - label_str = "#" + "".join([c * 2 for c in label_str[1:]]) + color_str = "#" + "".join([c * 2 for c in color_str[1:]]) + glyph_label = _LabelledTex.color_str_to_label(color_str) for mob in result: - mob.label_str = label_str + mob.glyph_label = glyph_label except: pass return result @@ -100,6 +105,19 @@ class MTex(VMobject): if self.organize_left_to_right: self.organize_submobjects_left_to_right() + @staticmethod + def label_to_color_tuple(n): + # Get a unique color different from black, + # or the svg file will not include the color information. + rgb = n + 1 + rg, b = divmod(rgb, 256) + r, g = divmod(rg, 256) + return r, g, b + + @staticmethod + def get_neighbouring_pairs(iterable): + return list(adjacent_pairs(iterable))[:-1] + def add_tex_span(self, span_tuple, script_type=0): if script_type == 0: # Should be additionally labelled. @@ -231,7 +249,7 @@ class MTex(VMobject): indices_with_labels.append((len(tex_string), 0, 0, -1)) result = tex_string[: indices_with_labels[0][0]] - index_with_label_pairs = list(adjacent_pairs(indices_with_labels))[:-1] + index_with_label_pairs = MTex.get_neighbouring_pairs(indices_with_labels) for index_with_label, next_index_with_label in index_with_label_pairs: index, flag, _, label = index_with_label # Adding one more pair of braces will help maintain the glyghs of tex file... @@ -249,34 +267,25 @@ class MTex(VMobject): result += tex_string[index : next_index_with_label[0]] return result - @staticmethod - def label_to_color_tuple(n): - # Get a unique color different from black, - # or the svg file will not include the color information. - rgb = n + 1 - rg, b = divmod(rgb, 256) - r, g = divmod(rg, 256) - return r, g, b - - @staticmethod - def color_str_to_label(color): - return int(color[1:], 16) - 1 - def build_submobjects(self): # Simply pack together adjacent mobjects with the same label. new_submobjects = [] new_glyphs = [] - current_label_str = "" + current_glyph_label = -1 for submob in self.submobjects: - if submob.label_str == current_label_str: + if submob.glyph_label == current_glyph_label: new_glyphs.append(submob) else: if new_glyphs: - new_submobjects.append(VGroup(*new_glyphs)) + new_submobject = VGroup(*new_glyphs) + new_submobject.submob_label = current_glyph_label + new_submobjects.append(new_submobject) new_glyphs = [submob] - current_label_str = submob.label_str + current_glyph_label = submob.glyph_label if new_glyphs: - new_submobjects.append(VGroup(*new_glyphs)) + new_submobject = VGroup(*new_glyphs) + new_submobject.submob_label = current_glyph_label + new_submobjects.append(new_submobject) self.set_submobjects(new_submobjects) return self @@ -289,7 +298,7 @@ class MTex(VMobject): if tex_span.script_type != 0 for index in span_tuple ]) - script_span_with_type_pair = list(adjacent_pairs(script_spans_with_types))[:-1] + script_span_with_type_pair = MTex.get_neighbouring_pairs(script_spans_with_types) for span_with_type_0, span_with_type_1 in script_span_with_type_pair: index_0, span_tuple_0, script_type_0 = span_with_type_0 index_1, span_tuple_1, script_type_1 = span_with_type_1 @@ -319,9 +328,8 @@ class MTex(VMobject): for span_tuple in span_tuples ]))) return VGroup(*filter( - lambda submob: submob.label_str \ - and MTex.color_str_to_label(submob.label_str) in labels, - it.chain(*self.submobjects) + lambda submob: submob.submob_label in labels, + self.submobjects )) def find_span_components_of_custom_span(self, custom_span_tuple, partial_result=[]): @@ -367,33 +375,30 @@ class MTex(VMobject): self.set_color_by_tex(tex, color) return self - def index_of_glyph(self, glyph): - submobs = self.submobjects - submobs_len = len(submobs) - result = 0 - while result < submobs_len and glyph not in submobs[result]: - result += 1 - if result == submobs_len: - raise ValueError("Unable to find glyph in tex") - return result + def indices_of_part(self, part): + return [ + i for i, submob in enumerate(self.submobjects) + if submob in part + ] + + def indices_of_part_by_tex(self, tex, index=0): + part = self.get_part_by_tex(tex, index=index) + return self.indices_of_part(part) def slice_of_part(self, part): - # Only finds where the head and the tail of `part` are. - begin_index = self.index_of_glyph(part[0]) - end_index = self.index_of_glyph(part[-1]) - if begin_index > end_index: - raise ValueError("Unable to find part in tex") - return slice(begin_index, end_index + 1) + indices = self.indices_of_part(part) + return slice(indices[0], indices[-1] + 1) def slice_of_part_by_tex(self, tex, index=0): part = self.get_part_by_tex(tex, index=index) return self.slice_of_part(part) def index_of_part(self, part): - return self.slice_of_part(part).start + return self.indices_of_part(part)[0] def index_of_part_by_tex(self, tex, index=0): - return self.slice_of_part_by_tex(tex, index=index).start + part = self.get_part_by_tex(tex, index=index) + return self.index_of_part(part) def get_all_isolated_substrings(self): tex_string = self.tex_string From 17d31045b2cb492b89ed48e7d45dbd18e11ab779 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Sun, 5 Dec 2021 11:45:42 +0800 Subject: [PATCH 19/30] Add `TransformMatchingMTex` --- manimlib/mobject/svg/mtex_mobject.py | 44 ++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 625f7f2f..af6466e9 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -27,25 +27,42 @@ class _LabelledTex(SVGMobject): } @staticmethod - def color_str_to_label(color): - return int(color[1:], 16) - 1 + def color_str_to_label(color_str): + if len(color_str) == 4: + # "#RGB" => "#RRGGBB" + color_str = "#" + "".join([c * 2 for c in color_str[1:]]) + return int(color_str[1:], 16) - 1 + + @staticmethod + def href_str_to_id(href_str): + match_obj = re.match(r"^#g(\d+)-(\d+)$", href_str) + if not match_obj: + return -1 + return match_obj.group(2) def get_mobjects_from(self, element): result = super().get_mobjects_from(element) for mob in result: if not hasattr(mob, "glyph_label"): mob.glyph_label = -1 + if not hasattr(mob, "glyph_id"): + mob.glyph_id = -1 try: color_str = element.getAttribute("fill") if color_str: - if len(color_str) == 4: - # "#RGB" => "#RRGGBB" - color_str = "#" + "".join([c * 2 for c in color_str[1:]]) glyph_label = _LabelledTex.color_str_to_label(color_str) for mob in result: mob.glyph_label = glyph_label except: pass + try: + href_str = element.getAttribute("xlink:href") + if href_str: + glyph_id = _LabelledTex.href_str_to_id(href_str) + for mob in result: + mob.glyph_id = glyph_id + except: + pass return result @@ -270,22 +287,23 @@ class MTex(VMobject): def build_submobjects(self): # Simply pack together adjacent mobjects with the same label. new_submobjects = [] + def append_new_submobject(glyphs): + if glyphs: + submobject = VGroup(*glyphs) + submobject.submob_label = glyphs[0].glyph_label + submobject.submob_id_tuple = tuple([glyph.glyph_id for glyph in glyphs]) + new_submobjects.append(submobject) + new_glyphs = [] current_glyph_label = -1 for submob in self.submobjects: if submob.glyph_label == current_glyph_label: new_glyphs.append(submob) else: - if new_glyphs: - new_submobject = VGroup(*new_glyphs) - new_submobject.submob_label = current_glyph_label - new_submobjects.append(new_submobject) + append_new_submobject(new_glyphs) new_glyphs = [submob] current_glyph_label = submob.glyph_label - if new_glyphs: - new_submobject = VGroup(*new_glyphs) - new_submobject.submob_label = current_glyph_label - new_submobjects.append(new_submobject) + append_new_submobject(new_glyphs) self.set_submobjects(new_submobjects) return self From 5a1f00b1cbca668b611368a62aa6cc584499d169 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Sun, 5 Dec 2021 11:46:15 +0800 Subject: [PATCH 20/30] Add `TransformMatchingMTex` --- manimlib/animation/transform_matching_parts.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/manimlib/animation/transform_matching_parts.py b/manimlib/animation/transform_matching_parts.py index 3ee228a9..58ce0461 100644 --- a/manimlib/animation/transform_matching_parts.py +++ b/manimlib/animation/transform_matching_parts.py @@ -139,3 +139,18 @@ class TransformMatchingTex(TransformMatchingParts): @staticmethod def get_mobject_key(mobject): return mobject.get_tex() + + +class TransformMatchingMTex(TransformMatchingParts): + CONFIG = { + "mobject_type": VMobject, + "group_type": VGroup, + } + + @staticmethod + def get_mobject_parts(mobject): + return mobject.submobjects + + @staticmethod + def get_mobject_key(mobject): + return mobject.submob_id_tuple From 1d466cb2998bfeb486103ab49acb094a6cfb184f Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Sun, 5 Dec 2021 22:17:09 +0800 Subject: [PATCH 21/30] Add Exception for `indices_of_part()` --- manimlib/mobject/svg/mtex_mobject.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index af6466e9..42c47bf7 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -394,10 +394,13 @@ class MTex(VMobject): return self def indices_of_part(self, part): - return [ + indices = [ i for i, submob in enumerate(self.submobjects) if submob in part ] + if not indices: + raise ValueError("Failed to find part in tex") + return indices def indices_of_part_by_tex(self, tex, index=0): part = self.get_part_by_tex(tex, index=index) From 88030881211ea1d05f7aa2a8714a296bee9608e1 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Mon, 6 Dec 2021 09:44:59 +0800 Subject: [PATCH 22/30] Fix bugs concerned with child environments --- manimlib/mobject/svg/mtex_mobject.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 42c47bf7..3e1a14f8 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -97,7 +97,7 @@ class MTex(VMobject): def __init__(self, tex_string, **kwargs): super().__init__(**kwargs) - self.tex_string = tex_string + self.tex_string = tex_string.strip("\n") self.parse_tex() full_tex = self.get_tex_file_body() @@ -168,8 +168,17 @@ class MTex(VMobject): if left_brace_indices: self.raise_tex_parsing_error() + # Braces leading by `\begin` and `\end` shouldn't be marked. + eliminated_left_brace_indices = [] + for match_obj in re.finditer(r"(\\begin|\\end)((\{\w+\})+)", self.tex_string): + for left_brace_match_obj in re.finditer(r"\{", match_obj.group(2)): + eliminated_left_brace_indices.append( + match_obj.span(2)[0] + left_brace_match_obj.span()[0] + ) + for span_tuple in span_tuples: - self.add_tex_span(span_tuple) + if span_tuple[0] not in eliminated_left_brace_indices: + self.add_tex_span(span_tuple) def break_up_by_scripts(self): tex_string = self.tex_string @@ -240,10 +249,10 @@ class MTex(VMobject): def get_tex_file_body(self): new_tex = self.get_modified_expression() - if self.math_mode: - new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}" - new_tex = self.alignment + "\n" + new_tex + if self.math_mode: + new_tex = "\n".join(["\\begin{align*}", new_tex, "\\end{align*}"]) + new_tex = "\n".join([self.alignment, new_tex]) tex_config = get_tex_config() return tex_config["tex_body"].replace( From 4631508b7d3471e29307fd9cb51181dee01345db Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Mon, 6 Dec 2021 13:48:17 +0800 Subject: [PATCH 23/30] Add `get_tex()` method --- manimlib/mobject/svg/mtex_mobject.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 3e1a14f8..0ed1bc9f 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -171,9 +171,10 @@ class MTex(VMobject): # Braces leading by `\begin` and `\end` shouldn't be marked. eliminated_left_brace_indices = [] for match_obj in re.finditer(r"(\\begin|\\end)((\{\w+\})+)", self.tex_string): + match_obj_index = match_obj.span(2)[0] for left_brace_match_obj in re.finditer(r"\{", match_obj.group(2)): eliminated_left_brace_indices.append( - match_obj.span(2)[0] + left_brace_match_obj.span()[0] + match_obj_index + left_brace_match_obj.span()[0] ) for span_tuple in span_tuples: @@ -331,7 +332,7 @@ class MTex(VMobject): index_1, span_tuple_1, script_type_1 = span_with_type_1 if index_0 != index_1: continue - if script_type_0 == 2 and script_type_1 == 1: + if not (script_type_0 == 1 and script_type_1 == 2): continue submob_slice_0 = self.slice_of_part( self.get_part_by_span_tuples([span_tuple_0]) @@ -430,6 +431,9 @@ class MTex(VMobject): part = self.get_part_by_tex(tex, index=index) return self.index_of_part(part) + def get_tex(self): + return self.tex_string + def get_all_isolated_substrings(self): tex_string = self.tex_string return remove_list_redundancies([ From d7dcc9d76f44ab1be522e9b958bb50947f571310 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Tue, 7 Dec 2021 00:32:12 +0800 Subject: [PATCH 24/30] Recover file --- manimlib/animation/transform_matching_parts.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/manimlib/animation/transform_matching_parts.py b/manimlib/animation/transform_matching_parts.py index 58ce0461..3ee228a9 100644 --- a/manimlib/animation/transform_matching_parts.py +++ b/manimlib/animation/transform_matching_parts.py @@ -139,18 +139,3 @@ class TransformMatchingTex(TransformMatchingParts): @staticmethod def get_mobject_key(mobject): return mobject.get_tex() - - -class TransformMatchingMTex(TransformMatchingParts): - CONFIG = { - "mobject_type": VMobject, - "group_type": VGroup, - } - - @staticmethod - def get_mobject_parts(mobject): - return mobject.submobjects - - @staticmethod - def get_mobject_key(mobject): - return mobject.submob_id_tuple From 88d863c1d7af2376d2406e0d0b849bd9fc8442a3 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Tue, 7 Dec 2021 00:34:07 +0800 Subject: [PATCH 25/30] Support `get_tex()` for submobjects of `MTex` --- manimlib/mobject/svg/mtex_mobject.py | 86 +++++++++++++++++----------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 0ed1bc9f..c5ae188e 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -1,5 +1,6 @@ import itertools as it import re +from types import MethodType from manimlib.mobject.svg.svg_mobject import SVGMobject from manimlib.mobject.types.vectorized_mobject import VMobject @@ -33,20 +34,11 @@ class _LabelledTex(SVGMobject): color_str = "#" + "".join([c * 2 for c in color_str[1:]]) return int(color_str[1:], 16) - 1 - @staticmethod - def href_str_to_id(href_str): - match_obj = re.match(r"^#g(\d+)-(\d+)$", href_str) - if not match_obj: - return -1 - return match_obj.group(2) - def get_mobjects_from(self, element): result = super().get_mobjects_from(element) for mob in result: if not hasattr(mob, "glyph_label"): mob.glyph_label = -1 - if not hasattr(mob, "glyph_id"): - mob.glyph_id = -1 try: color_str = element.getAttribute("fill") if color_str: @@ -55,21 +47,13 @@ class _LabelledTex(SVGMobject): mob.glyph_label = glyph_label except: pass - try: - href_str = element.getAttribute("xlink:href") - if href_str: - glyph_id = _LabelledTex.href_str_to_id(href_str) - for mob in result: - mob.glyph_id = glyph_id - except: - pass return result class _TexSpan(object): def __init__(self, script_type, label): # script_type: 0 for normal, 1 for subscript, 2 for superscript. - # Only those spans with `label != -1` will be colored. + # Only those spans with `script_type == 0` will be colored. self.script_type = script_type self.label = label self.containing_labels = [] @@ -113,6 +97,7 @@ class MTex(VMobject): ]) self.build_submobjects() self.sort_scripts_in_tex_order() + self.assign_submob_tex_strings() self.init_colors() self.set_color_by_tex_to_color_map(self.tex_to_color_map) @@ -135,13 +120,11 @@ class MTex(VMobject): def get_neighbouring_pairs(iterable): return list(adjacent_pairs(iterable))[:-1] - def add_tex_span(self, span_tuple, script_type=0): + def add_tex_span(self, span_tuple, script_type=0, label=-1): if script_type == 0: # Should be additionally labelled. label = self.current_label self.current_label += 1 - else: - label = -1 tex_span = _TexSpan(script_type, label) self.tex_spans_dict[span_tuple] = tex_span @@ -203,12 +186,13 @@ class MTex(VMobject): label = self.tex_spans_dict[content_span].label self.add_tex_span( (token_begin, content_span[1]), - script_type=script_type + script_type=script_type, + label=label ) def break_up_by_additional_strings(self): additional_strings_to_break_up = remove_list_redundancies([ - *self.isolate, *self.tex_to_color_map.keys() + *self.isolate, *self.tex_to_color_map.keys(), self.tex_string ]) if "" in additional_strings_to_break_up: additional_strings_to_break_up.remove("") @@ -218,28 +202,29 @@ class MTex(VMobject): tex_string = self.tex_string all_span_tuples = list(self.tex_spans_dict.keys()) for string in additional_strings_to_break_up: - # Only matches non-overlapping strings. + # Only matches non-crossing strings. for match_obj in re.finditer(re.escape(string), tex_string): all_span_tuples.append(match_obj.span()) - # Deconstruct spans with subscripts & superscripts. script_spans_dict = dict([ span_tuple[::-1] for span_tuple, tex_span in self.tex_spans_dict.items() if tex_span.script_type != 0 ]) for span_begin, span_end in all_span_tuples: - while span_end in script_spans_dict: - span_end = script_spans_dict[span_end] - if span_begin >= span_end: - continue + if span_begin in script_spans_dict.values(): + # Deconstruct spans with subscripts & superscripts. + while span_end in script_spans_dict: + span_end = script_spans_dict[span_end] + if span_begin >= span_end: + continue span_tuple = (span_begin, span_end) if span_tuple not in self.tex_spans_dict: self.add_tex_span(span_tuple) def analyse_containing_labels(self): for span_0, tex_span_0 in self.tex_spans_dict.items(): - if tex_span_0.label == -1: + if tex_span_0.script_type != 0: continue for span_1, tex_span_1 in self.tex_spans_dict.items(): if span_1[0] <= span_0[0] and span_0[1] <= span_1[1]: @@ -269,11 +254,11 @@ class MTex(VMobject): indices_with_labels = sorted([ (span_tuple[i], i, span_tuple[1 - i], tex_span.label) for span_tuple, tex_span in self.tex_spans_dict.items() - if tex_span.label != -1 + if tex_span.script_type == 0 for i in range(2) ], key=lambda t: (t[0], -t[1], -t[2])) # Add one more item to ensure all the substrings are joined. - indices_with_labels.append((len(tex_string), 0, 0, -1)) + indices_with_labels.append((len(tex_string), 0, 0, 0)) result = tex_string[: indices_with_labels[0][0]] index_with_label_pairs = MTex.get_neighbouring_pairs(indices_with_labels) @@ -301,7 +286,6 @@ class MTex(VMobject): if glyphs: submobject = VGroup(*glyphs) submobject.submob_label = glyphs[0].glyph_label - submobject.submob_id_tuple = tuple([glyph.glyph_id for glyph in glyphs]) new_submobjects.append(submobject) new_glyphs = [] @@ -350,6 +334,42 @@ class MTex(VMobject): ]) return self + def assign_submob_tex_strings(self): + tex_string = self.tex_string + label_dict = { + tex_span.label: (span_tuple, tex_span.containing_labels) + for span_tuple, tex_span in self.tex_spans_dict.items() + if tex_span.script_type == 0 + } + # Use tex strings with "_", "^" included. + label_dict.update({ + tex_span.label: (span_tuple, tex_span.containing_labels) + for span_tuple, tex_span in self.tex_spans_dict.items() + if tex_span.script_type != 0 + }) + + curr_labels = [submob.submob_label for submob in self.submobjects] + prev_labels = [curr_labels[-1], *curr_labels[:-1]] + next_labels = [*curr_labels[1:], curr_labels[0]] + tex_string_spans = [] + for curr_submob_label, prev_submob_label, next_submob_label in zip( + curr_labels, prev_labels, next_labels + ): + curr_span_tuple, containing_labels = label_dict[curr_submob_label] + prev_span_tuple, _ = label_dict[prev_submob_label] + next_span_tuple, _ = label_dict[next_submob_label] + tex_string_spans.append([ + prev_span_tuple[1] if prev_submob_label in containing_labels else curr_span_tuple[0], + next_span_tuple[0] if next_submob_label in containing_labels else curr_span_tuple[1] + ]) + tex_string_spans[0][0] = label_dict[curr_labels[0]][0][0] + tex_string_spans[-1][1] = label_dict[curr_labels[-1]][0][1] + for submob, tex_string_span in zip(self.submobjects, tex_string_spans): + submob.tex_string = tex_string[slice(*tex_string_span)] + # Support `get_tex()` method here. + submob.get_tex = MethodType(lambda inst: inst.tex_string, submob) + return self + def get_part_by_span_tuples(self, span_tuples): labels = remove_list_redundancies(list(it.chain(*[ self.tex_spans_dict[span_tuple].containing_labels From 744916507cef8bba557828136f5c8ee13a522388 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Tue, 7 Dec 2021 12:55:52 +0800 Subject: [PATCH 26/30] Add a debugging method --- manimlib/mobject/svg/mtex_mobject.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index c5ae188e..fa2bd0b9 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -335,6 +335,7 @@ class MTex(VMobject): return self def assign_submob_tex_strings(self): + # Not sure whether this is the best practice... tex_string = self.tex_string label_dict = { tex_span.label: (span_tuple, tex_span.containing_labels) @@ -461,6 +462,13 @@ class MTex(VMobject): for span_tuple in self.tex_spans_dict.keys() ]) + def print_tex_strings_of_submobjects(self): + # For debugging + # Working with `index_labels()` + print(f"Submobjects of \"{self.get_tex()}\":") + for i, submob in enumerate(self.submobjects): + print(f"{i}: \"{submob.get_tex()}\"") + class MTexText(MTex): CONFIG = { From 00f72da49334071d0062142a2d3cb710edec8ec8 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Tue, 7 Dec 2021 13:17:48 +0800 Subject: [PATCH 27/30] Some small refactor --- manimlib/mobject/svg/mtex_mobject.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index fa2bd0b9..ce52cb72 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -96,8 +96,6 @@ class MTex(VMobject): for submob in tex_hash_to_mob_map[hash_val] ]) self.build_submobjects() - self.sort_scripts_in_tex_order() - self.assign_submob_tex_strings() self.init_colors() self.set_color_by_tex_to_color_map(self.tex_to_color_map) @@ -280,6 +278,11 @@ class MTex(VMobject): return result def build_submobjects(self): + self.group_submobjects() + self.sort_scripts_in_tex_order() + self.assign_submob_tex_strings() + + def group_submobjects(self): # Simply pack together adjacent mobjects with the same label. new_submobjects = [] def append_new_submobject(glyphs): @@ -299,7 +302,6 @@ class MTex(VMobject): current_glyph_label = submob.glyph_label append_new_submobject(new_glyphs) self.set_submobjects(new_submobjects) - return self def sort_scripts_in_tex_order(self): # LaTeX always puts superscripts before subscripts. @@ -332,19 +334,18 @@ class MTex(VMobject): *submobs[submob_slice_1], *submobs[submob_slice_0.stop :] ]) - return self def assign_submob_tex_strings(self): # Not sure whether this is the best practice... tex_string = self.tex_string label_dict = { - tex_span.label: (span_tuple, tex_span.containing_labels) + tex_span.label: span_tuple for span_tuple, tex_span in self.tex_spans_dict.items() if tex_span.script_type == 0 } # Use tex strings with "_", "^" included. label_dict.update({ - tex_span.label: (span_tuple, tex_span.containing_labels) + tex_span.label: span_tuple for span_tuple, tex_span in self.tex_spans_dict.items() if tex_span.script_type != 0 }) @@ -356,20 +357,20 @@ class MTex(VMobject): for curr_submob_label, prev_submob_label, next_submob_label in zip( curr_labels, prev_labels, next_labels ): - curr_span_tuple, containing_labels = label_dict[curr_submob_label] - prev_span_tuple, _ = label_dict[prev_submob_label] - next_span_tuple, _ = label_dict[next_submob_label] + curr_span_tuple = label_dict[curr_submob_label] + prev_span_tuple = label_dict[prev_submob_label] + next_span_tuple = label_dict[next_submob_label] + containing_labels = self.tex_spans_dict[curr_span_tuple].containing_labels tex_string_spans.append([ prev_span_tuple[1] if prev_submob_label in containing_labels else curr_span_tuple[0], next_span_tuple[0] if next_submob_label in containing_labels else curr_span_tuple[1] ]) - tex_string_spans[0][0] = label_dict[curr_labels[0]][0][0] - tex_string_spans[-1][1] = label_dict[curr_labels[-1]][0][1] + tex_string_spans[0][0] = label_dict[curr_labels[0]][0] + tex_string_spans[-1][1] = label_dict[curr_labels[-1]][1] for submob, tex_string_span in zip(self.submobjects, tex_string_spans): submob.tex_string = tex_string[slice(*tex_string_span)] # Support `get_tex()` method here. submob.get_tex = MethodType(lambda inst: inst.tex_string, submob) - return self def get_part_by_span_tuples(self, span_tuples): labels = remove_list_redundancies(list(it.chain(*[ @@ -464,7 +465,7 @@ class MTex(VMobject): def print_tex_strings_of_submobjects(self): # For debugging - # Working with `index_labels()` + # Work with `index_labels()` print(f"Submobjects of \"{self.get_tex()}\":") for i, submob in enumerate(self.submobjects): print(f"{i}: \"{submob.get_tex()}\"") From 6821a7c20ed6f47a79bc57b9121259e78d5dd8de Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Tue, 7 Dec 2021 14:12:08 +0800 Subject: [PATCH 28/30] Handle empty strings --- manimlib/mobject/svg/mtex_mobject.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index ce52cb72..e151b106 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -81,7 +81,11 @@ class MTex(VMobject): def __init__(self, tex_string, **kwargs): super().__init__(**kwargs) - self.tex_string = tex_string.strip("\n") + tex_string = tex_string.strip("\n") + # Prevent from passing an empty string. + if not tex_string: + tex_string = "\\quad" + self.tex_string = tex_string self.parse_tex() full_tex = self.get_tex_file_body() @@ -278,6 +282,8 @@ class MTex(VMobject): return result def build_submobjects(self): + if not self.submobjects: + return self.group_submobjects() self.sort_scripts_in_tex_order() self.assign_submob_tex_strings() From 155839bde997b9315ef35733d29c9306c22a37a7 Mon Sep 17 00:00:00 2001 From: Michael W <50232075+YishiMichael@users.noreply.github.com> Date: Mon, 13 Dec 2021 12:46:29 +0800 Subject: [PATCH 29/30] Add `unbreakable_commands` parameter --- manimlib/mobject/svg/mtex_mobject.py | 125 ++++++++++++++++----------- 1 file changed, 75 insertions(+), 50 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index e151b106..930edf92 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -76,6 +76,7 @@ class MTex(VMobject): "alignment": "\\centering", "math_mode": True, "isolate": [], + "unbreakable_commands": ["\\begin", "\\end"], "tex_to_color_map": {}, } @@ -122,6 +123,10 @@ class MTex(VMobject): def get_neighbouring_pairs(iterable): return list(adjacent_pairs(iterable))[:-1] + @staticmethod + def contains(span_0, span_1): + return span_0[0] <= span_1[0] and span_1[1] <= span_0[1] + def add_tex_span(self, span_tuple, script_type=0, label=-1): if script_type == 0: # Should be additionally labelled. @@ -137,12 +142,14 @@ class MTex(VMobject): self.break_up_by_braces() self.break_up_by_scripts() self.break_up_by_additional_strings() + self.merge_unbreakable_strings() self.analyse_containing_labels() def break_up_by_braces(self): + tex_string = self.tex_string span_tuples = [] left_brace_indices = [] - for match_obj in re.finditer(r"(? Date: Mon, 13 Dec 2021 21:01:27 +0800 Subject: [PATCH 30/30] Some refactors - Split out `_TexParser` class - Replace `math_mode` parameter with `tex_environment` - Fix the bug that braces following even number of backslashes aren't matched --- manimlib/mobject/svg/mtex_mobject.py | 407 ++++++++++++++------------- 1 file changed, 210 insertions(+), 197 deletions(-) diff --git a/manimlib/mobject/svg/mtex_mobject.py b/manimlib/mobject/svg/mtex_mobject.py index 930edf92..9168693a 100644 --- a/manimlib/mobject/svg/mtex_mobject.py +++ b/manimlib/mobject/svg/mtex_mobject.py @@ -18,6 +18,10 @@ SCALE_FACTOR_PER_FONT_POINT = 0.001 tex_hash_to_mob_map = {} +def _get_neighbouring_pairs(iterable): + return list(adjacent_pairs(iterable))[:-1] + + class _LabelledTex(SVGMobject): CONFIG = { "height": None, @@ -65,6 +69,185 @@ class _TexSpan(object): ]) + ")" +class _TexParser(object): + def __init__(self, mtex): + self.tex_string = mtex.tex_string + strings_to_break_up = remove_list_redundancies([ + *mtex.isolate, *mtex.tex_to_color_map.keys(), mtex.tex_string + ]) + if "" in strings_to_break_up: + strings_to_break_up.remove("") + unbreakable_commands = mtex.unbreakable_commands + + self.tex_spans_dict = {} + self.current_label = 0 + self.break_up_by_braces() + self.break_up_by_scripts() + self.break_up_by_additional_strings(strings_to_break_up) + self.merge_unbreakable_commands(unbreakable_commands) + self.analyse_containing_labels() + + @staticmethod + def label_to_color_tuple(n): + # Get a unique color different from black, + # or the svg file will not include the color information. + rgb = n + 1 + rg, b = divmod(rgb, 256) + r, g = divmod(rg, 256) + return r, g, b + + @staticmethod + def contains(span_0, span_1): + return span_0[0] <= span_1[0] and span_1[1] <= span_0[1] + + def add_tex_span(self, span_tuple, script_type=0, label=-1): + if script_type == 0: + # Should be additionally labelled. + label = self.current_label + self.current_label += 1 + + tex_span = _TexSpan(script_type, label) + self.tex_spans_dict[span_tuple] = tex_span + + def break_up_by_braces(self): + tex_string = self.tex_string + span_tuples = [] + left_brace_indices = [] + for match_obj in re.finditer(r"(\\*)(\{|\})", tex_string): + # Braces following even numbers of backslashes are counted. + if len(match_obj.group(1)) % 2 == 1: + continue + if match_obj.group(2) == "{": + left_brace_index = match_obj.span(2)[0] + left_brace_indices.append(left_brace_index) + else: + left_brace_index = left_brace_indices.pop() + right_brace_index = match_obj.span(2)[1] + span_tuples.append((left_brace_index, right_brace_index)) + if left_brace_indices: + self.raise_tex_parsing_error() + + self.paired_braces_tuples = span_tuples + for span_tuple in span_tuples: + self.add_tex_span(span_tuple) + + def break_up_by_scripts(self): + tex_string = self.tex_string + brace_indices_dict = dict(self.tex_spans_dict.keys()) + for match_obj in re.finditer(r"((?= span_end: + continue + span_tuple = (span_begin, span_end) + if span_tuple not in self.tex_spans_dict: + self.add_tex_span(span_tuple) + + def merge_unbreakable_commands(self, unbreakable_commands): + tex_string = self.tex_string + command_merge_spans = [] + brace_indices_dict = dict(self.paired_braces_tuples) + # Braces leading by `unbreakable_commands` shouldn't be marked. + for command in unbreakable_commands: + for match_obj in re.finditer(re.escape(command), tex_string): + merge_begin_index = match_obj.span()[1] + merge_end_index = merge_begin_index + if merge_end_index not in brace_indices_dict: + continue + while merge_end_index in brace_indices_dict: + merge_end_index = brace_indices_dict[merge_end_index] + command_merge_spans.append((merge_begin_index, merge_end_index)) + + self.tex_spans_dict = { + span_tuple: tex_span + for span_tuple, tex_span in self.tex_spans_dict.items() + if all([ + not _TexParser.contains(merge_span, span_tuple) + for merge_span in command_merge_spans + ]) + } + + def analyse_containing_labels(self): + for span_0, tex_span_0 in self.tex_spans_dict.items(): + if tex_span_0.script_type != 0: + continue + for span_1, tex_span_1 in self.tex_spans_dict.items(): + if _TexParser.contains(span_1, span_0): + tex_span_1.containing_labels.append(tex_span_0.label) + + def get_labelled_expression(self): + tex_string = self.tex_string + if not self.tex_spans_dict: + return tex_string + + indices_with_labels = sorted([ + (span_tuple[i], i, span_tuple[1 - i], tex_span.label) + for span_tuple, tex_span in self.tex_spans_dict.items() + if tex_span.script_type == 0 + for i in range(2) + ], key=lambda t: (t[0], -t[1], -t[2])) + # Add one more item to ensure all the substrings are joined. + indices_with_labels.append((len(tex_string), 0, 0, 0)) + + result = tex_string[: indices_with_labels[0][0]] + index_with_label_pairs = _get_neighbouring_pairs(indices_with_labels) + for index_with_label, next_index_with_label in index_with_label_pairs: + index, flag, _, label = index_with_label + next_index, *_ = next_index_with_label + # Adding one more pair of braces will help maintain the glyghs of tex file... + if flag == 0: + color_tuple = _TexParser.label_to_color_tuple(label) + result += "".join([ + "{{", + "\\color[RGB]", + "{", + ",".join(map(str, color_tuple)), + "}" + ]) + else: + result += "}}" + result += tex_string[index : next_index] + return result + + def raise_tex_parsing_error(self): + raise ValueError(f"Failed to parse tex: \"{self.tex_string}\"") + + class MTex(VMobject): CONFIG = { "fill_opacity": 1.0, @@ -74,7 +257,7 @@ class MTex(VMobject): "height": None, "organize_left_to_right": False, "alignment": "\\centering", - "math_mode": True, + "tex_environment": "align*", "isolate": [], "unbreakable_commands": ["\\begin", "\\end"], "tex_to_color_map": {}, @@ -82,14 +265,13 @@ class MTex(VMobject): def __init__(self, tex_string, **kwargs): super().__init__(**kwargs) - tex_string = tex_string.strip("\n") - # Prevent from passing an empty string. - if not tex_string: - tex_string = "\\quad" - self.tex_string = tex_string - self.parse_tex() + self.tex_string = MTex.modify_tex_string(tex_string) - full_tex = self.get_tex_file_body() + tex_parser = _TexParser(self) + self.tex_spans_dict = tex_parser.tex_spans_dict + + new_tex = tex_parser.get_labelled_expression() + full_tex = self.get_tex_file_body(new_tex) hash_val = hash(full_tex) if hash_val not in tex_hash_to_mob_map: with display_during_execution(f"Writing \"{tex_string}\""): @@ -111,160 +293,22 @@ class MTex(VMobject): self.organize_submobjects_left_to_right() @staticmethod - def label_to_color_tuple(n): - # Get a unique color different from black, - # or the svg file will not include the color information. - rgb = n + 1 - rg, b = divmod(rgb, 256) - r, g = divmod(rg, 256) - return r, g, b + def modify_tex_string(tex_string): + result = tex_string.strip("\n") + # Prevent from passing an empty string. + if not result: + result = "\\quad" + return result - @staticmethod - def get_neighbouring_pairs(iterable): - return list(adjacent_pairs(iterable))[:-1] - - @staticmethod - def contains(span_0, span_1): - return span_0[0] <= span_1[0] and span_1[1] <= span_0[1] - - def add_tex_span(self, span_tuple, script_type=0, label=-1): - if script_type == 0: - # Should be additionally labelled. - label = self.current_label - self.current_label += 1 - - tex_span = _TexSpan(script_type, label) - self.tex_spans_dict[span_tuple] = tex_span - - def parse_tex(self): - self.tex_spans_dict = {} - self.current_label = 0 - self.break_up_by_braces() - self.break_up_by_scripts() - self.break_up_by_additional_strings() - self.merge_unbreakable_strings() - self.analyse_containing_labels() - - def break_up_by_braces(self): - tex_string = self.tex_string - span_tuples = [] - left_brace_indices = [] - for match_obj in re.finditer(r"(?= span_end: - continue - span_tuple = (span_begin, span_end) - if span_tuple not in self.tex_spans_dict: - self.add_tex_span(span_tuple) - - def merge_unbreakable_strings(self): - tex_string = self.tex_string - command_merge_spans = [] - brace_indices_dict = dict(self.paired_braces_tuples) - # Braces leading by `unbreakable_commands` shouldn't be marked. - for command in self.unbreakable_commands: - for match_obj in re.finditer(re.escape(command), tex_string): - merge_begin_index = match_obj.span()[1] - merge_end_index = merge_begin_index - if merge_end_index not in brace_indices_dict: - continue - while merge_end_index in brace_indices_dict: - merge_end_index = brace_indices_dict[merge_end_index] - command_merge_spans.append((merge_begin_index, merge_end_index)) - - if not command_merge_spans: - return - self.tex_spans_dict = { - span_tuple: tex_span - for span_tuple, tex_span in self.tex_spans_dict.items() - if all([ - not MTex.contains(merge_span, span_tuple) - for merge_span in command_merge_spans + def get_tex_file_body(self, new_tex): + if self.tex_environment: + new_tex = "\n".join([ + f"\\begin{{{self.tex_environment}}}", + new_tex, + f"\\end{{{self.tex_environment}}}" ]) - } - - def analyse_containing_labels(self): - for span_0, tex_span_0 in self.tex_spans_dict.items(): - if tex_span_0.script_type != 0: - continue - for span_1, tex_span_1 in self.tex_spans_dict.items(): - if MTex.contains(span_1, span_0): - tex_span_1.containing_labels.append(tex_span_0.label) - - def raise_tex_parsing_error(self): - raise ValueError(f"Failed to parse tex: \"{self.tex_string}\"") - - def get_tex_file_body(self): - new_tex = self.get_modified_expression() - - if self.math_mode: - new_tex = "\n".join(["\\begin{align*}", new_tex, "\\end{align*}"]) - new_tex = "\n".join([self.alignment, new_tex]) + if self.alignment: + new_tex = "\n".join([self.alignment, new_tex]) tex_config = get_tex_config() return tex_config["tex_body"].replace( @@ -272,40 +316,6 @@ class MTex(VMobject): new_tex ) - def get_modified_expression(self): - tex_string = self.tex_string - if not self.tex_spans_dict: - return tex_string - - indices_with_labels = sorted([ - (span_tuple[i], i, span_tuple[1 - i], tex_span.label) - for span_tuple, tex_span in self.tex_spans_dict.items() - if tex_span.script_type == 0 - for i in range(2) - ], key=lambda t: (t[0], -t[1], -t[2])) - # Add one more item to ensure all the substrings are joined. - indices_with_labels.append((len(tex_string), 0, 0, 0)) - - result = tex_string[: indices_with_labels[0][0]] - index_with_label_pairs = MTex.get_neighbouring_pairs(indices_with_labels) - for index_with_label, next_index_with_label in index_with_label_pairs: - index, flag, _, label = index_with_label - next_index, *_ = next_index_with_label - # Adding one more pair of braces will help maintain the glyghs of tex file... - if flag == 0: - color_tuple = MTex.label_to_color_tuple(label) - result += "".join([ - "{{", - "\\color[RGB]", - "{", - ",".join(map(str, color_tuple)), - "}" - ]) - else: - result += "}}" - result += tex_string[index : next_index] - return result - def build_submobjects(self): if not self.submobjects: return @@ -339,10 +349,11 @@ class MTex(VMobject): # This function sorts the submobjects of scripts in the order of tex given. index_and_span_list = sorted([ (index, span_tuple) - for span_tuple in self.script_spans + for span_tuple, tex_span in self.tex_spans_dict.items() + if tex_span.script_type != 0 for index in span_tuple ]) - index_and_span_pair = MTex.get_neighbouring_pairs(index_and_span_list) + index_and_span_pair = _get_neighbouring_pairs(index_and_span_list) for index_and_span_0, index_and_span_1 in index_and_span_pair: index_0, span_tuple_0 = index_and_span_0 index_1, span_tuple_1 = index_and_span_1 @@ -497,12 +508,14 @@ class MTex(VMobject): def print_tex_strings_of_submobjects(self): # For debugging # Work with `index_labels()` + print("\n") print(f"Submobjects of \"{self.get_tex()}\":") for i, submob in enumerate(self.submobjects): print(f"{i}: \"{submob.get_tex()}\"") + print("\n") class MTexText(MTex): CONFIG = { - "math_mode": False, + "tex_environment": None, }