mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
Merge branch 'master' of github.com:3b1b/manim into video-work
This commit is contained in:
commit
e09a264cab
19 changed files with 865 additions and 1068 deletions
|
@ -1,14 +1,23 @@
|
||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
Unreleased
|
v1.4.1
|
||||||
----------
|
------
|
||||||
|
|
||||||
|
Fixed bugs
|
||||||
|
^^^^^^^^^^
|
||||||
|
- `#1724 <https://github.com/3b1b/manim/pull/1724>`__: Temporarily fixed boolean operations' bug
|
||||||
|
- `d2e0811 <https://github.com/3b1b/manim/commit/d2e0811285f7908e71a65e664fec88b1af1c6144>`__: Import ``Iterable`` from ``collections.abc`` instead of ``collections`` which is deprecated since python 3.9
|
||||||
|
|
||||||
|
v1.4.0
|
||||||
|
------
|
||||||
|
|
||||||
Fixed bugs
|
Fixed bugs
|
||||||
^^^^^^^^^^
|
^^^^^^^^^^
|
||||||
- `f1996f8 <https://github.com/3b1b/manim/pull/1697/commits/f1996f8479f9e33d626b3b66e9eb6995ce231d86>`__: Temporarily fixed ``Lightbulb``
|
- `f1996f8 <https://github.com/3b1b/manim/pull/1697/commits/f1996f8479f9e33d626b3b66e9eb6995ce231d86>`__: Temporarily fixed ``Lightbulb``
|
||||||
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Fixed some bugs of ``SVGMobject``
|
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Fixed some bugs of ``SVGMobject``
|
||||||
- `#1717 <https://github.com/3b1b/manim/pull/1717>`__: Fixed some bugs of SVG path string parser
|
- `#1717 <https://github.com/3b1b/manim/pull/1717>`__: Fixed some bugs of SVG path string parser
|
||||||
|
- `#1720 <https://github.com/3b1b/manim/pull/1720>`__: Fixed some bugs of ``MTex``
|
||||||
|
|
||||||
New Features
|
New Features
|
||||||
^^^^^^^^^^^^
|
^^^^^^^^^^^^
|
||||||
|
@ -16,6 +25,8 @@ New Features
|
||||||
- `#1704 <https://github.com/3b1b/manim/pull/1704>`__: Added ``lable_buff`` config parameter for ``Brace``
|
- `#1704 <https://github.com/3b1b/manim/pull/1704>`__: Added ``lable_buff`` config parameter for ``Brace``
|
||||||
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Added support for ``rotate skewX skewY`` transform in SVG
|
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Added support for ``rotate skewX skewY`` transform in SVG
|
||||||
- `#1717 <https://github.com/3b1b/manim/pull/1717>`__: Added style support to ``SVGMobject``
|
- `#1717 <https://github.com/3b1b/manim/pull/1717>`__: Added style support to ``SVGMobject``
|
||||||
|
- `#1719 <https://github.com/3b1b/manim/pull/1719>`__: Added parser to <style> element of SVG
|
||||||
|
- `#1719 <https://github.com/3b1b/manim/pull/1719>`__: Added support for <line> element in ``SVGMobject``
|
||||||
|
|
||||||
Refactor
|
Refactor
|
||||||
^^^^^^^^
|
^^^^^^^^
|
||||||
|
@ -24,6 +35,12 @@ Refactor
|
||||||
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Refactored SVG path string parser
|
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Refactored SVG path string parser
|
||||||
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Allowed ``Mobject.scale`` to receive iterable ``scale_factor``
|
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Allowed ``Mobject.scale`` to receive iterable ``scale_factor``
|
||||||
- `#1716 <https://github.com/3b1b/manim/pull/1716>`__: Refactored ``MTex``
|
- `#1716 <https://github.com/3b1b/manim/pull/1716>`__: Refactored ``MTex``
|
||||||
|
- `#1721 <https://github.com/3b1b/manim/pull/1721>`__: Improved config helper (``manimgl --config``)
|
||||||
|
- `#1723 <https://github.com/3b1b/manim/pull/1723>`__: Refactored ``MTex``
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
- `#1719 <https://github.com/3b1b/manim/pull/1719>`__: Added dependency on python package `cssselect2 <https://github.com/Kozea/cssselect2>`__
|
||||||
|
|
||||||
|
|
||||||
v1.3.0
|
v1.3.0
|
||||||
|
@ -88,7 +105,7 @@ Refactor
|
||||||
Dependencies
|
Dependencies
|
||||||
^^^^^^^^^^^^
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
- `#1675 <https://github.com/3b1b/manim/pull/1675>`__: Added dependency on python packages `skia-pathops <https://github.com/fonttools/skia-pathops>`__
|
- `#1675 <https://github.com/3b1b/manim/pull/1675>`__: Added dependency on python package `skia-pathops <https://github.com/fonttools/skia-pathops>`__
|
||||||
|
|
||||||
v1.2.0
|
v1.2.0
|
||||||
------
|
------
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from manimlib.animation.animation import Animation
|
from manimlib.animation.animation import Animation
|
||||||
from manimlib.animation.composition import Succession
|
from manimlib.animation.composition import Succession
|
||||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||||
from manimlib.mobject.mobject import Group
|
|
||||||
from manimlib.utils.bezier import integer_interpolate
|
from manimlib.utils.bezier import integer_interpolate
|
||||||
from manimlib.utils.config_ops import digest_config
|
from manimlib.utils.config_ops import digest_config
|
||||||
from manimlib.utils.rate_functions import linear
|
from manimlib.utils.rate_functions import linear
|
||||||
|
|
|
@ -17,7 +17,6 @@ class Broadcast(LaggedStart):
|
||||||
"remover": True,
|
"remover": True,
|
||||||
"lag_ratio": 0.2,
|
"lag_ratio": 0.2,
|
||||||
"run_time": 3,
|
"run_time": 3,
|
||||||
"remover": True,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, focal_point, **kwargs):
|
def __init__(self, focal_point, **kwargs):
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import itertools as it
|
||||||
|
|
||||||
from manimlib.animation.composition import AnimationGroup
|
from manimlib.animation.composition import AnimationGroup
|
||||||
from manimlib.animation.fading import FadeTransformPieces
|
from manimlib.animation.fading import FadeTransformPieces
|
||||||
from manimlib.animation.fading import FadeInFromPoint
|
from manimlib.animation.fading import FadeInFromPoint
|
||||||
from manimlib.animation.fading import FadeOutToPoint
|
from manimlib.animation.fading import FadeOutToPoint
|
||||||
|
from manimlib.animation.transform import ReplacementTransform
|
||||||
from manimlib.animation.transform import Transform
|
from manimlib.animation.transform import Transform
|
||||||
|
|
||||||
from manimlib.mobject.mobject import Mobject
|
from manimlib.mobject.mobject import Mobject
|
||||||
from manimlib.mobject.mobject import Group
|
from manimlib.mobject.mobject import Group
|
||||||
|
from manimlib.mobject.svg.mtex_mobject import MTex
|
||||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||||
from manimlib.utils.config_ops import digest_config
|
from manimlib.utils.config_ops import digest_config
|
||||||
|
from manimlib.utils.iterables import remove_list_redundancies
|
||||||
|
|
||||||
|
|
||||||
class TransformMatchingParts(AnimationGroup):
|
class TransformMatchingParts(AnimationGroup):
|
||||||
|
@ -139,3 +143,108 @@ class TransformMatchingTex(TransformMatchingParts):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_mobject_key(mobject):
|
def get_mobject_key(mobject):
|
||||||
return mobject.get_tex()
|
return mobject.get_tex()
|
||||||
|
|
||||||
|
|
||||||
|
class TransformMatchingMTex(AnimationGroup):
|
||||||
|
CONFIG = {
|
||||||
|
"key_map": dict(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, source_mobject, target_mobject, **kwargs):
|
||||||
|
digest_config(self, kwargs)
|
||||||
|
assert isinstance(source_mobject, MTex)
|
||||||
|
assert isinstance(target_mobject, MTex)
|
||||||
|
anims = []
|
||||||
|
rest_source_submobs = source_mobject.submobjects.copy()
|
||||||
|
rest_target_submobs = target_mobject.submobjects.copy()
|
||||||
|
|
||||||
|
def add_anim_from(anim_class, func, source_attr, target_attr=None):
|
||||||
|
if target_attr is None:
|
||||||
|
target_attr = source_attr
|
||||||
|
source_parts = func(source_mobject, source_attr)
|
||||||
|
target_parts = func(target_mobject, target_attr)
|
||||||
|
filtered_source_parts = [
|
||||||
|
submob_part for submob_part in source_parts
|
||||||
|
if all([
|
||||||
|
submob in rest_source_submobs
|
||||||
|
for submob in submob_part
|
||||||
|
])
|
||||||
|
]
|
||||||
|
filtered_target_parts = [
|
||||||
|
submob_part for submob_part in target_parts
|
||||||
|
if all([
|
||||||
|
submob in rest_target_submobs
|
||||||
|
for submob in submob_part
|
||||||
|
])
|
||||||
|
]
|
||||||
|
if not (filtered_source_parts and filtered_target_parts):
|
||||||
|
return
|
||||||
|
anims.append(anim_class(
|
||||||
|
VGroup(*filtered_source_parts),
|
||||||
|
VGroup(*filtered_target_parts),
|
||||||
|
**kwargs
|
||||||
|
))
|
||||||
|
for submob in it.chain(*filtered_source_parts):
|
||||||
|
rest_source_submobs.remove(submob)
|
||||||
|
for submob in it.chain(*filtered_target_parts):
|
||||||
|
rest_target_submobs.remove(submob)
|
||||||
|
|
||||||
|
def get_submobs_from_keys(mobject, keys):
|
||||||
|
if not isinstance(keys, tuple):
|
||||||
|
keys = (keys,)
|
||||||
|
indices = []
|
||||||
|
for key in keys:
|
||||||
|
if isinstance(key, int):
|
||||||
|
indices.append(key)
|
||||||
|
elif isinstance(key, range):
|
||||||
|
indices.extend(key)
|
||||||
|
elif isinstance(key, str):
|
||||||
|
all_parts = mobject.get_parts_by_tex(key)
|
||||||
|
indices.extend(it.chain(*[
|
||||||
|
mobject.indices_of_part(part) for part in all_parts
|
||||||
|
]))
|
||||||
|
else:
|
||||||
|
raise TypeError(key)
|
||||||
|
return VGroup(VGroup(*[
|
||||||
|
mobject[i] for i in remove_list_redundancies(indices)
|
||||||
|
]))
|
||||||
|
|
||||||
|
for source_key, target_key in self.key_map.items():
|
||||||
|
add_anim_from(
|
||||||
|
ReplacementTransform, get_submobs_from_keys,
|
||||||
|
source_key, target_key
|
||||||
|
)
|
||||||
|
|
||||||
|
common_specified_substrings = sorted(list(
|
||||||
|
set(source_mobject.get_specified_substrings()).intersection(
|
||||||
|
target_mobject.get_specified_substrings()
|
||||||
|
)
|
||||||
|
), key=len, reverse=True)
|
||||||
|
for part_tex_string in common_specified_substrings:
|
||||||
|
add_anim_from(
|
||||||
|
FadeTransformPieces, MTex.get_parts_by_tex, part_tex_string
|
||||||
|
)
|
||||||
|
|
||||||
|
common_submob_tex_strings = {
|
||||||
|
source_submob.get_tex() for source_submob in source_mobject
|
||||||
|
}.intersection({
|
||||||
|
target_submob.get_tex() for target_submob in target_mobject
|
||||||
|
})
|
||||||
|
for tex_string in common_submob_tex_strings:
|
||||||
|
add_anim_from(
|
||||||
|
FadeTransformPieces,
|
||||||
|
lambda mobject, attr: VGroup(*[
|
||||||
|
VGroup(mob) for mob in mobject
|
||||||
|
if mob.get_tex() == attr
|
||||||
|
]),
|
||||||
|
tex_string
|
||||||
|
)
|
||||||
|
|
||||||
|
anims.append(FadeOutToPoint(
|
||||||
|
VGroup(*rest_source_submobs), target_mobject.get_center(), **kwargs
|
||||||
|
))
|
||||||
|
anims.append(FadeInFromPoint(
|
||||||
|
VGroup(*rest_target_submobs), source_mobject.get_center(), **kwargs
|
||||||
|
))
|
||||||
|
|
||||||
|
super().__init__(*anims)
|
||||||
|
|
|
@ -41,7 +41,7 @@ def _convert_skia_path_to_vmobject(path, vmobject):
|
||||||
vmobject.add_quadratic_bezier_curve_to(*points)
|
vmobject.add_quadratic_bezier_curve_to(*points)
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Unsupported: {path_verb}")
|
raise Exception(f"Unsupported: {path_verb}")
|
||||||
return vmobject
|
return vmobject.reverse_points()
|
||||||
|
|
||||||
|
|
||||||
class Union(VMobject):
|
class Union(VMobject):
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from isosurfaces import plot_isoline
|
||||||
|
|
||||||
from manimlib.constants import *
|
from manimlib.constants import *
|
||||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||||
from manimlib.utils.config_ops import digest_config
|
from manimlib.utils.config_ops import digest_config
|
||||||
|
@ -70,3 +72,40 @@ class FunctionGraph(ParametricCurve):
|
||||||
|
|
||||||
def get_point_from_function(self, x):
|
def get_point_from_function(self, x):
|
||||||
return self.t_func(x)
|
return self.t_func(x)
|
||||||
|
|
||||||
|
|
||||||
|
class ImplicitFunction(VMobject):
|
||||||
|
CONFIG = {
|
||||||
|
"x_range": [-FRAME_X_RADIUS, FRAME_X_RADIUS],
|
||||||
|
"y_range": [-FRAME_Y_RADIUS, FRAME_Y_RADIUS],
|
||||||
|
"min_depth": 5,
|
||||||
|
"max_quads": 1500,
|
||||||
|
"use_smoothing": True
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, func, x_range=None, y_range=None, **kwargs):
|
||||||
|
digest_config(self, kwargs)
|
||||||
|
self.function = func
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def init_points(self):
|
||||||
|
p_min, p_max = (
|
||||||
|
np.array([self.x_range[0], self.y_range[0]]),
|
||||||
|
np.array([self.x_range[1], self.y_range[1]]),
|
||||||
|
)
|
||||||
|
curves = plot_isoline(
|
||||||
|
fn=lambda u: self.function(u[0], u[1]),
|
||||||
|
pmin=p_min,
|
||||||
|
pmax=p_max,
|
||||||
|
min_depth=self.min_depth,
|
||||||
|
max_quads=self.max_quads,
|
||||||
|
) # returns a list of lists of 2D points
|
||||||
|
curves = [
|
||||||
|
np.pad(curve, [(0, 0), (0, 1)]) for curve in curves if curve != []
|
||||||
|
] # add z coord as 0
|
||||||
|
for curve in curves:
|
||||||
|
self.start_new_path(curve[0])
|
||||||
|
self.add_points_as_corners(curve[1:])
|
||||||
|
if self.use_smoothing:
|
||||||
|
self.make_smooth()
|
||||||
|
return self
|
||||||
|
|
|
@ -608,8 +608,8 @@ class Arrow(Line):
|
||||||
self.insert_tip_anchor()
|
self.insert_tip_anchor()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def init_colors(self, override=True):
|
def init_colors(self):
|
||||||
super().init_colors(override)
|
super().init_colors()
|
||||||
self.create_tip_with_stroke_width()
|
self.create_tip_with_stroke_width()
|
||||||
|
|
||||||
def get_arc_length(self):
|
def get_arc_length(self):
|
||||||
|
@ -849,6 +849,11 @@ class Polygon(VMobject):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class Polyline(Polygon):
|
||||||
|
def init_points(self):
|
||||||
|
self.set_points_as_corners(self.vertices)
|
||||||
|
|
||||||
|
|
||||||
class RegularPolygon(Polygon):
|
class RegularPolygon(Polygon):
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"start_angle": None,
|
"start_angle": None,
|
||||||
|
|
|
@ -112,7 +112,7 @@ class Matrix(VMobject):
|
||||||
"\\left[",
|
"\\left[",
|
||||||
"\\begin{array}{c}",
|
"\\begin{array}{c}",
|
||||||
*height * ["\\quad \\\\"],
|
*height * ["\\quad \\\\"],
|
||||||
"\\end{array}"
|
"\\end{array}",
|
||||||
"\\right]",
|
"\\right]",
|
||||||
]))[0]
|
]))[0]
|
||||||
bracket_pair.set_height(
|
bracket_pair.set_height(
|
||||||
|
|
|
@ -4,7 +4,7 @@ import random
|
||||||
import sys
|
import sys
|
||||||
import moderngl
|
import moderngl
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from collections import Iterable
|
from collections.abc import Iterable
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
@ -109,8 +109,8 @@ class Mobject(object):
|
||||||
"reflectiveness": self.reflectiveness,
|
"reflectiveness": self.reflectiveness,
|
||||||
}
|
}
|
||||||
|
|
||||||
def init_colors(self, override=True):
|
def init_colors(self):
|
||||||
self.set_color(self.color, self.opacity, override)
|
self.set_color(self.color, self.opacity)
|
||||||
|
|
||||||
def init_points(self):
|
def init_points(self):
|
||||||
# Typically implemented in subclass, unlpess purposefully left blank
|
# Typically implemented in subclass, unlpess purposefully left blank
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import itertools as it
|
import itertools as it
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
from types import MethodType
|
from types import MethodType
|
||||||
|
|
||||||
|
from manimlib.constants import BLACK
|
||||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||||
|
from manimlib.utils.color import color_to_int_rgb
|
||||||
from manimlib.utils.iterables import adjacent_pairs
|
from manimlib.utils.iterables import adjacent_pairs
|
||||||
from manimlib.utils.iterables import remove_list_redundancies
|
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 tex_to_svg_file
|
||||||
|
@ -20,16 +21,14 @@ SCALE_FACTOR_PER_FONT_POINT = 0.001
|
||||||
TEX_HASH_TO_MOB_MAP = {}
|
TEX_HASH_TO_MOB_MAP = {}
|
||||||
|
|
||||||
|
|
||||||
def _contains(span_0, span_1):
|
|
||||||
return span_0[0] <= span_1[0] and span_1[1] <= span_0[1]
|
|
||||||
|
|
||||||
|
|
||||||
def _get_neighbouring_pairs(iterable):
|
def _get_neighbouring_pairs(iterable):
|
||||||
return list(adjacent_pairs(iterable))[:-1]
|
return list(adjacent_pairs(iterable))[:-1]
|
||||||
|
|
||||||
|
|
||||||
class _PlainTex(SVGMobject):
|
class _TexSVG(SVGMobject):
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
|
"color": BLACK,
|
||||||
|
"stroke_width": 0,
|
||||||
"height": None,
|
"height": None,
|
||||||
"path_string_config": {
|
"path_string_config": {
|
||||||
"should_subdivide_sharp_curves": True,
|
"should_subdivide_sharp_curves": True,
|
||||||
|
@ -38,239 +37,367 @@ class _PlainTex(SVGMobject):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _LabelledTex(_PlainTex):
|
|
||||||
def __init__(self, file_name=None, **kwargs):
|
|
||||||
super().__init__(file_name, **kwargs)
|
|
||||||
for glyph in self:
|
|
||||||
glyph.glyph_label = _LabelledTex.color_str_to_label(glyph.fill_color)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
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
|
|
||||||
|
|
||||||
def get_mobjects_from(self, element, style):
|
|
||||||
result = super().get_mobjects_from(element, style)
|
|
||||||
for mob in result:
|
|
||||||
if not hasattr(mob, "glyph_label"):
|
|
||||||
mob.glyph_label = -1
|
|
||||||
try:
|
|
||||||
color_str = element.getAttribute("fill")
|
|
||||||
if color_str:
|
|
||||||
glyph_label = _LabelledTex.color_str_to_label(color_str)
|
|
||||||
for mob in result:
|
|
||||||
mob.glyph_label = glyph_label
|
|
||||||
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 `script_type == 0` will be colored.
|
|
||||||
self.script_type = script_type
|
|
||||||
self.label = label
|
|
||||||
self.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 _TexParser(object):
|
class _TexParser(object):
|
||||||
def __init__(self, tex_string, additional_substrings):
|
def __init__(self, tex_string, additional_substrings):
|
||||||
self.tex_string = tex_string
|
self.tex_string = tex_string
|
||||||
self.tex_spans_dict = {}
|
self.whitespace_indices = self.get_whitespace_indices()
|
||||||
|
self.backslash_indices = self.get_backslash_indices()
|
||||||
|
self.script_indices = self.get_script_indices()
|
||||||
|
self.brace_indices_dict = self.get_brace_indices_dict()
|
||||||
|
self.tex_span_list = []
|
||||||
|
self.script_span_to_char_dict = {}
|
||||||
|
self.script_span_to_tex_span_dict = {}
|
||||||
|
self.neighbouring_script_span_pairs = []
|
||||||
self.specified_substrings = []
|
self.specified_substrings = []
|
||||||
self.current_label = 0
|
|
||||||
self.brace_index_pairs = self.get_brace_index_pairs()
|
|
||||||
self.add_tex_span((0, len(tex_string)))
|
self.add_tex_span((0, len(tex_string)))
|
||||||
self.break_up_by_double_braces()
|
|
||||||
self.break_up_by_scripts()
|
self.break_up_by_scripts()
|
||||||
|
self.break_up_by_double_braces()
|
||||||
self.break_up_by_additional_substrings(additional_substrings)
|
self.break_up_by_additional_substrings(additional_substrings)
|
||||||
self.check_if_overlap()
|
self.tex_span_list.sort(key=lambda t: (t[0], -t[1]))
|
||||||
self.analyse_containing_labels()
|
self.specified_substrings = remove_list_redundancies(
|
||||||
self.specified_substrings = remove_list_redundancies(self.specified_substrings)
|
self.specified_substrings
|
||||||
|
)
|
||||||
|
self.containing_labels_dict = self.get_containing_labels_dict()
|
||||||
|
|
||||||
@staticmethod
|
def add_tex_span(self, tex_span):
|
||||||
def label_to_color_tuple(rgb):
|
if tex_span not in self.tex_span_list:
|
||||||
# Get a unique color different from black,
|
self.tex_span_list.append(tex_span)
|
||||||
# or the svg file will not include the color information.
|
|
||||||
rg, b = divmod(rgb, 256)
|
|
||||||
r, g = divmod(rg, 256)
|
|
||||||
return r, g, b
|
|
||||||
|
|
||||||
def add_tex_span(self, span_tuple, script_type=0, label=-1):
|
def get_whitespace_indices(self):
|
||||||
if span_tuple in self.tex_spans_dict:
|
return [
|
||||||
return
|
match_obj.start()
|
||||||
|
for match_obj in re.finditer(r"\s", self.tex_string)
|
||||||
|
]
|
||||||
|
|
||||||
if script_type == 0:
|
def get_backslash_indices(self):
|
||||||
# Should be additionally labelled.
|
# Newlines (`\\`) don't count.
|
||||||
label = self.current_label
|
return [
|
||||||
self.current_label += 1
|
match_obj.end() - 1
|
||||||
|
for match_obj in re.finditer(r"\\+", self.tex_string)
|
||||||
|
if len(match_obj.group()) % 2 == 1
|
||||||
|
]
|
||||||
|
|
||||||
tex_span = _TexSpan(script_type, label)
|
def filter_out_escaped_characters(self, indices):
|
||||||
self.tex_spans_dict[span_tuple] = tex_span
|
return list(filter(
|
||||||
|
lambda index: index - 1 not in self.backslash_indices,
|
||||||
|
indices
|
||||||
|
))
|
||||||
|
|
||||||
def add_specified_substring(self, span_tuple):
|
def get_script_indices(self):
|
||||||
substring = self.tex_string[slice(*span_tuple)]
|
return self.filter_out_escaped_characters([
|
||||||
self.specified_substrings.append(substring)
|
match_obj.start()
|
||||||
|
for match_obj in re.finditer(r"[_^]", self.tex_string)
|
||||||
|
])
|
||||||
|
|
||||||
def get_brace_index_pairs(self):
|
def get_brace_indices_dict(self):
|
||||||
result = []
|
tex_string = self.tex_string
|
||||||
left_brace_indices = []
|
indices = self.filter_out_escaped_characters([
|
||||||
for match_obj in re.finditer(r"(\\*)(\{|\})", self.tex_string):
|
match_obj.start()
|
||||||
# Braces following even numbers of backslashes are counted.
|
for match_obj in re.finditer(r"[{}]", tex_string)
|
||||||
if len(match_obj.group(1)) % 2 == 1:
|
])
|
||||||
continue
|
result = {}
|
||||||
if match_obj.group(2) == "{":
|
left_brace_indices_stack = []
|
||||||
left_brace_index = match_obj.span(2)[0]
|
for index in indices:
|
||||||
left_brace_indices.append(left_brace_index)
|
if tex_string[index] == "{":
|
||||||
|
left_brace_indices_stack.append(index)
|
||||||
else:
|
else:
|
||||||
left_brace_index = left_brace_indices.pop()
|
left_brace_index = left_brace_indices_stack.pop()
|
||||||
right_brace_index = match_obj.span(2)[1]
|
result[left_brace_index] = index
|
||||||
result.append((left_brace_index, right_brace_index))
|
|
||||||
if left_brace_indices:
|
|
||||||
self.raise_tex_parsing_error("unmatched braces")
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def break_up_by_double_braces(self):
|
|
||||||
# Match paired double braces (`{{...}}`).
|
|
||||||
skip_pair = False
|
|
||||||
for prev_span_tuple, span_tuple in _get_neighbouring_pairs(
|
|
||||||
self.brace_index_pairs
|
|
||||||
):
|
|
||||||
if skip_pair:
|
|
||||||
skip_pair = False
|
|
||||||
continue
|
|
||||||
if all([
|
|
||||||
span_tuple[0] == prev_span_tuple[0] - 1,
|
|
||||||
span_tuple[1] == prev_span_tuple[1] + 1
|
|
||||||
]):
|
|
||||||
self.add_tex_span(span_tuple)
|
|
||||||
self.add_specified_substring(span_tuple)
|
|
||||||
skip_pair = True
|
|
||||||
|
|
||||||
def break_up_by_scripts(self):
|
def break_up_by_scripts(self):
|
||||||
# Match subscripts & superscripts.
|
# Match subscripts & superscripts.
|
||||||
tex_string = self.tex_string
|
tex_string = self.tex_string
|
||||||
brace_indices_dict = dict(self.brace_index_pairs)
|
whitespace_indices = self.whitespace_indices
|
||||||
for match_obj in re.finditer(r"((?<!\\)(_|\^)\s*)|(\s+(_|\^)\s*)", tex_string):
|
brace_indices_dict = self.brace_indices_dict
|
||||||
script_type = 1 if "_" in match_obj.group() else 2
|
script_spans = []
|
||||||
token_begin, token_end = match_obj.span()
|
for script_index in self.script_indices:
|
||||||
if token_end in brace_indices_dict:
|
script_char = tex_string[script_index]
|
||||||
content_span = (token_end, brace_indices_dict[token_end])
|
extended_begin = script_index
|
||||||
|
while extended_begin - 1 in whitespace_indices:
|
||||||
|
extended_begin -= 1
|
||||||
|
script_begin = script_index + 1
|
||||||
|
while script_begin in whitespace_indices:
|
||||||
|
script_begin += 1
|
||||||
|
if script_begin in brace_indices_dict.keys():
|
||||||
|
script_end = brace_indices_dict[script_begin] + 1
|
||||||
else:
|
else:
|
||||||
content_match_obj = re.match(r"\w|\\[a-zA-Z]+", tex_string[token_end:])
|
pattern = re.compile(r"[a-zA-Z0-9]|\\[a-zA-Z]+")
|
||||||
if not content_match_obj:
|
match_obj = pattern.match(tex_string, pos=script_begin)
|
||||||
self.raise_tex_parsing_error("unclear subscript/superscript")
|
if not match_obj:
|
||||||
content_span = tuple([
|
script_name = {
|
||||||
index + token_end for index in content_match_obj.span()
|
"_": "subscript",
|
||||||
])
|
"^": "superscript"
|
||||||
self.add_tex_span(content_span)
|
}[script_char]
|
||||||
label = self.tex_spans_dict[content_span].label
|
log.warning(
|
||||||
self.add_tex_span(
|
f"Unclear {script_name} detected while parsing. "
|
||||||
(token_begin, content_span[1]),
|
"Please use braces to clarify"
|
||||||
script_type=script_type,
|
)
|
||||||
label=label
|
continue
|
||||||
)
|
script_end = match_obj.end()
|
||||||
|
tex_span = (script_begin, script_end)
|
||||||
|
script_span = (extended_begin, script_end)
|
||||||
|
script_spans.append(script_span)
|
||||||
|
self.add_tex_span(tex_span)
|
||||||
|
self.script_span_to_char_dict[script_span] = script_char
|
||||||
|
self.script_span_to_tex_span_dict[script_span] = tex_span
|
||||||
|
|
||||||
|
if not script_spans:
|
||||||
|
return
|
||||||
|
|
||||||
|
_, sorted_script_spans = zip(*sorted([
|
||||||
|
(index, script_span)
|
||||||
|
for script_span in script_spans
|
||||||
|
for index in script_span
|
||||||
|
]))
|
||||||
|
for span_0, span_1 in _get_neighbouring_pairs(sorted_script_spans):
|
||||||
|
if span_0[1] == span_1[0]:
|
||||||
|
self.neighbouring_script_span_pairs.append((span_0, span_1))
|
||||||
|
|
||||||
|
def break_up_by_double_braces(self):
|
||||||
|
# Match paired double braces (`{{...}}`).
|
||||||
|
tex_string = self.tex_string
|
||||||
|
reversed_indices_dict = dict(
|
||||||
|
item[::-1] for item in self.brace_indices_dict.items()
|
||||||
|
)
|
||||||
|
skip = False
|
||||||
|
for prev_right_index, right_index in _get_neighbouring_pairs(
|
||||||
|
list(reversed_indices_dict.keys())
|
||||||
|
):
|
||||||
|
if skip:
|
||||||
|
skip = False
|
||||||
|
continue
|
||||||
|
if right_index != prev_right_index + 1:
|
||||||
|
continue
|
||||||
|
left_index = reversed_indices_dict[right_index]
|
||||||
|
prev_left_index = reversed_indices_dict[prev_right_index]
|
||||||
|
if left_index != prev_left_index - 1:
|
||||||
|
continue
|
||||||
|
tex_span = (left_index, right_index + 1)
|
||||||
|
self.add_tex_span(tex_span)
|
||||||
|
self.specified_substrings.append(tex_string[slice(*tex_span)])
|
||||||
|
skip = True
|
||||||
|
|
||||||
def break_up_by_additional_substrings(self, additional_substrings):
|
def break_up_by_additional_substrings(self, additional_substrings):
|
||||||
tex_string = self.tex_string
|
stripped_substrings = sorted(remove_list_redundancies([
|
||||||
all_span_tuples = []
|
string.strip()
|
||||||
for string in additional_substrings:
|
for string in additional_substrings
|
||||||
# Only match non-crossing strings.
|
]))
|
||||||
for match_obj in re.finditer(re.escape(string), tex_string):
|
if "" in stripped_substrings:
|
||||||
all_span_tuples.append(match_obj.span())
|
stripped_substrings.remove("")
|
||||||
|
|
||||||
script_spans_dict = dict([
|
tex_string = self.tex_string
|
||||||
span_tuple[::-1]
|
all_tex_spans = []
|
||||||
for span_tuple, tex_span in self.tex_spans_dict.items()
|
for string in stripped_substrings:
|
||||||
if tex_span.script_type != 0
|
match_objs = list(re.finditer(re.escape(string), tex_string))
|
||||||
|
if not match_objs:
|
||||||
|
continue
|
||||||
|
self.specified_substrings.append(string)
|
||||||
|
for match_obj in match_objs:
|
||||||
|
all_tex_spans.append(match_obj.span())
|
||||||
|
|
||||||
|
former_script_spans_dict = dict([
|
||||||
|
script_span_pair[0][::-1]
|
||||||
|
for script_span_pair in self.neighbouring_script_span_pairs
|
||||||
])
|
])
|
||||||
for span_begin, span_end in all_span_tuples:
|
for span_begin, span_end in all_tex_spans:
|
||||||
if span_end in script_spans_dict.values():
|
# Deconstruct spans containing one out of two scripts.
|
||||||
# Deconstruct spans with subscripts & superscripts.
|
if span_end in former_script_spans_dict.keys():
|
||||||
while span_end in script_spans_dict:
|
span_end = former_script_spans_dict[span_end]
|
||||||
span_end = script_spans_dict[span_end]
|
|
||||||
if span_begin >= span_end:
|
if span_begin >= span_end:
|
||||||
continue
|
continue
|
||||||
span_tuple = (span_begin, span_end)
|
self.add_tex_span((span_begin, span_end))
|
||||||
self.add_tex_span(span_tuple)
|
|
||||||
self.add_specified_substring(span_tuple)
|
|
||||||
|
|
||||||
def check_if_overlap(self):
|
def get_containing_labels_dict(self):
|
||||||
span_tuples = sorted(
|
tex_span_list = self.tex_span_list
|
||||||
self.tex_spans_dict.keys(),
|
result = {
|
||||||
key=lambda t: (t[0], -t[1])
|
tex_span: []
|
||||||
)
|
for tex_span in tex_span_list
|
||||||
overlapping_span_pairs = []
|
}
|
||||||
for i, span_0 in enumerate(span_tuples):
|
overlapping_tex_span_pairs = []
|
||||||
for span_1 in span_tuples[i + 1 :]:
|
for index_0, span_0 in enumerate(tex_span_list):
|
||||||
|
for index_1, span_1 in enumerate(tex_span_list[index_0:]):
|
||||||
if span_0[1] <= span_1[0]:
|
if span_0[1] <= span_1[0]:
|
||||||
continue
|
continue
|
||||||
if span_0[1] < span_1[1]:
|
if span_0[1] < span_1[1]:
|
||||||
overlapping_span_pairs.append((span_0, span_1))
|
overlapping_tex_span_pairs.append((span_0, span_1))
|
||||||
if overlapping_span_pairs:
|
result[span_0].append(index_0 + index_1)
|
||||||
|
if overlapping_tex_span_pairs:
|
||||||
tex_string = self.tex_string
|
tex_string = self.tex_string
|
||||||
log.error("Overlapping substring pairs occur in MTex:")
|
log.error("Partially overlapping substrings detected:")
|
||||||
for span_tuple_pair in overlapping_span_pairs:
|
for tex_span_pair in overlapping_tex_span_pairs:
|
||||||
log.error(", ".join(
|
log.error(", ".join(
|
||||||
f"\"{tex_string[slice(*span_tuple)]}\""
|
f"\"{tex_string[slice(*tex_span)]}\""
|
||||||
for span_tuple in span_tuple_pair
|
for tex_span in tex_span_pair
|
||||||
))
|
))
|
||||||
sys.exit(2)
|
raise ValueError
|
||||||
|
|
||||||
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 _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
|
|
||||||
|
|
||||||
# Remove the span of extire 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]))[1:]
|
|
||||||
|
|
||||||
result = tex_string[: indices_with_labels[0][0]]
|
|
||||||
for index_with_label, next_index_with_label in _get_neighbouring_pairs(
|
|
||||||
indices_with_labels
|
|
||||||
):
|
|
||||||
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
|
return result
|
||||||
|
|
||||||
def raise_tex_parsing_error(self, message):
|
def get_labelled_tex_string(self):
|
||||||
raise ValueError(f"Failed to parse tex ({message}): \"{self.tex_string}\"")
|
indices, _, flags, labels = zip(*sorted([
|
||||||
|
(*tex_span[::(1, -1)[flag]], flag, label)
|
||||||
|
for label, tex_span in enumerate(self.tex_span_list)
|
||||||
|
for flag in range(2)
|
||||||
|
], key=lambda t: (t[0], -t[2], -t[1])))
|
||||||
|
command_pieces = [
|
||||||
|
("{{" + self.get_color_command(label), "}}")[flag]
|
||||||
|
for flag, label in zip(flags, labels)
|
||||||
|
][1:-1]
|
||||||
|
command_pieces.insert(0, "")
|
||||||
|
string_pieces = [
|
||||||
|
self.tex_string[slice(*tex_span)]
|
||||||
|
for tex_span in _get_neighbouring_pairs(indices)
|
||||||
|
]
|
||||||
|
return "".join(it.chain(*zip(command_pieces, string_pieces)))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_color_command(label):
|
||||||
|
rg, b = divmod(label, 256)
|
||||||
|
r, g = divmod(rg, 256)
|
||||||
|
return "".join([
|
||||||
|
"\\color[RGB]",
|
||||||
|
"{",
|
||||||
|
",".join(map(str, (r, g, b))),
|
||||||
|
"}"
|
||||||
|
])
|
||||||
|
|
||||||
|
def get_sorted_submob_indices(self, submob_labels):
|
||||||
|
def script_span_to_submob_range(script_span):
|
||||||
|
tex_span = self.script_span_to_tex_span_dict[script_span]
|
||||||
|
submob_indices = [
|
||||||
|
index for index, label in enumerate(submob_labels)
|
||||||
|
if label in self.containing_labels_dict[tex_span]
|
||||||
|
]
|
||||||
|
return range(submob_indices[0], submob_indices[-1] + 1)
|
||||||
|
|
||||||
|
filtered_script_span_pairs = filter(
|
||||||
|
lambda script_span_pair: all([
|
||||||
|
self.script_span_to_char_dict[script_span] == character
|
||||||
|
for script_span, character in zip(script_span_pair, "_^")
|
||||||
|
]),
|
||||||
|
self.neighbouring_script_span_pairs
|
||||||
|
)
|
||||||
|
switch_range_pairs = sorted([
|
||||||
|
tuple([
|
||||||
|
script_span_to_submob_range(script_span)
|
||||||
|
for script_span in script_span_pair
|
||||||
|
])
|
||||||
|
for script_span_pair in filtered_script_span_pairs
|
||||||
|
], key=lambda t: (t[0].stop, -t[0].start))
|
||||||
|
result = list(range(len(submob_labels)))
|
||||||
|
for range_0, range_1 in switch_range_pairs:
|
||||||
|
result = [
|
||||||
|
*result[:range_1.start],
|
||||||
|
*result[range_0.start:range_0.stop],
|
||||||
|
*result[range_1.stop:range_0.start],
|
||||||
|
*result[range_1.start:range_1.stop],
|
||||||
|
*result[range_0.stop:]
|
||||||
|
]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_submob_tex_strings(self, submob_labels):
|
||||||
|
ordered_tex_spans = [
|
||||||
|
self.tex_span_list[label] for label in submob_labels
|
||||||
|
]
|
||||||
|
ordered_containing_labels = [
|
||||||
|
self.containing_labels_dict[tex_span]
|
||||||
|
for tex_span in ordered_tex_spans
|
||||||
|
]
|
||||||
|
ordered_span_begins, ordered_span_ends = zip(*ordered_tex_spans)
|
||||||
|
string_span_begins = [
|
||||||
|
prev_end if prev_label in containing_labels else curr_begin
|
||||||
|
for prev_end, prev_label, containing_labels, curr_begin in zip(
|
||||||
|
ordered_span_ends[:-1], submob_labels[:-1],
|
||||||
|
ordered_containing_labels[1:], ordered_span_begins[1:]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
string_span_begins.insert(0, ordered_span_begins[0])
|
||||||
|
string_span_ends = [
|
||||||
|
next_begin if next_label in containing_labels else curr_end
|
||||||
|
for next_begin, next_label, containing_labels, curr_end in zip(
|
||||||
|
ordered_span_begins[1:], submob_labels[1:],
|
||||||
|
ordered_containing_labels[:-1], ordered_span_ends[:-1]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
string_span_ends.append(ordered_span_ends[-1])
|
||||||
|
|
||||||
|
tex_string = self.tex_string
|
||||||
|
left_brace_indices = sorted(self.brace_indices_dict.keys())
|
||||||
|
right_brace_indices = sorted(self.brace_indices_dict.values())
|
||||||
|
ignored_indices = sorted(it.chain(
|
||||||
|
self.whitespace_indices,
|
||||||
|
left_brace_indices,
|
||||||
|
right_brace_indices,
|
||||||
|
self.script_indices
|
||||||
|
))
|
||||||
|
result = []
|
||||||
|
for span_begin, span_end in zip(string_span_begins, string_span_ends):
|
||||||
|
while span_begin in ignored_indices:
|
||||||
|
span_begin += 1
|
||||||
|
if span_begin >= span_end:
|
||||||
|
result.append("")
|
||||||
|
continue
|
||||||
|
while span_end - 1 in ignored_indices:
|
||||||
|
span_end -= 1
|
||||||
|
unclosed_left_brace = 0
|
||||||
|
unclosed_right_brace = 0
|
||||||
|
for index in range(span_begin, span_end):
|
||||||
|
if index in left_brace_indices:
|
||||||
|
unclosed_left_brace += 1
|
||||||
|
elif index in right_brace_indices:
|
||||||
|
if unclosed_left_brace == 0:
|
||||||
|
unclosed_right_brace += 1
|
||||||
|
else:
|
||||||
|
unclosed_left_brace -= 1
|
||||||
|
result.append("".join([
|
||||||
|
unclosed_right_brace * "{",
|
||||||
|
tex_string[span_begin:span_end],
|
||||||
|
unclosed_left_brace * "}"
|
||||||
|
]))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def find_span_components_of_custom_span(self, custom_span):
|
||||||
|
skipped_indices = sorted(it.chain(
|
||||||
|
self.whitespace_indices,
|
||||||
|
self.script_indices
|
||||||
|
))
|
||||||
|
tex_span_choices = sorted(filter(
|
||||||
|
lambda tex_span: all([
|
||||||
|
tex_span[0] >= custom_span[0],
|
||||||
|
tex_span[1] <= custom_span[1]
|
||||||
|
]),
|
||||||
|
self.tex_span_list
|
||||||
|
))
|
||||||
|
# Choose spans that reach the farthest.
|
||||||
|
tex_span_choices_dict = dict(tex_span_choices)
|
||||||
|
|
||||||
|
span_begin, span_end = custom_span
|
||||||
|
result = []
|
||||||
|
while span_begin != span_end:
|
||||||
|
if span_begin not in tex_span_choices_dict.keys():
|
||||||
|
if span_begin in skipped_indices:
|
||||||
|
span_begin += 1
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
next_begin = tex_span_choices_dict[span_begin]
|
||||||
|
result.append((span_begin, next_begin))
|
||||||
|
span_begin = next_begin
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_containing_labels_by_tex_spans(self, tex_spans):
|
||||||
|
return remove_list_redundancies(list(it.chain(*[
|
||||||
|
self.containing_labels_dict[tex_span]
|
||||||
|
for tex_span in tex_spans
|
||||||
|
])))
|
||||||
|
|
||||||
|
def get_specified_substrings(self):
|
||||||
|
return self.specified_substrings
|
||||||
|
|
||||||
|
def get_isolated_substrings(self):
|
||||||
|
return remove_list_redundancies([
|
||||||
|
self.tex_string[slice(*tex_span)]
|
||||||
|
for tex_span in self.tex_span_list
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
class MTex(VMobject):
|
class MTex(VMobject):
|
||||||
|
@ -282,7 +409,7 @@ class MTex(VMobject):
|
||||||
"tex_environment": "align*",
|
"tex_environment": "align*",
|
||||||
"isolate": [],
|
"isolate": [],
|
||||||
"tex_to_color_map": {},
|
"tex_to_color_map": {},
|
||||||
"generate_plain_tex_file": False,
|
"use_plain_tex": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, tex_string, **kwargs):
|
def __init__(self, tex_string, **kwargs):
|
||||||
|
@ -293,227 +420,138 @@ class MTex(VMobject):
|
||||||
tex_string = "\\quad"
|
tex_string = "\\quad"
|
||||||
self.tex_string = tex_string
|
self.tex_string = tex_string
|
||||||
|
|
||||||
self.generate_mobject()
|
self.__parser = _TexParser(
|
||||||
|
self.tex_string,
|
||||||
|
[*self.tex_to_color_map.keys(), *self.isolate]
|
||||||
|
)
|
||||||
|
mob = self.generate_mobject()
|
||||||
|
self.add(*mob.copy())
|
||||||
self.init_colors()
|
self.init_colors()
|
||||||
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
|
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
|
||||||
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
|
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
|
||||||
|
|
||||||
def get_additional_substrings_to_break_up(self):
|
@staticmethod
|
||||||
result = remove_list_redundancies([
|
def color_to_label(color):
|
||||||
*self.tex_to_color_map.keys(), *self.isolate
|
r, g, b = color_to_int_rgb(color)
|
||||||
])
|
rg = r * 256 + g
|
||||||
if "" in result:
|
return rg * 256 + b
|
||||||
result.remove("")
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_parser(self):
|
|
||||||
return _TexParser(self.tex_string, self.get_additional_substrings_to_break_up())
|
|
||||||
|
|
||||||
def generate_mobject(self):
|
def generate_mobject(self):
|
||||||
tex_string = self.tex_string
|
labelled_tex_string = self.__parser.get_labelled_tex_string()
|
||||||
tex_parser = self.get_parser()
|
labelled_tex_content = self.get_tex_file_content(labelled_tex_string)
|
||||||
self.tex_spans_dict = tex_parser.tex_spans_dict
|
hash_val = hash((labelled_tex_content, self.use_plain_tex))
|
||||||
self.specified_substrings = tex_parser.specified_substrings
|
|
||||||
|
|
||||||
plain_full_tex = self.get_tex_file_body(tex_string)
|
if hash_val in TEX_HASH_TO_MOB_MAP:
|
||||||
plain_hash_val = hash(plain_full_tex)
|
return TEX_HASH_TO_MOB_MAP[hash_val]
|
||||||
if plain_hash_val in TEX_HASH_TO_MOB_MAP:
|
|
||||||
self.add(*TEX_HASH_TO_MOB_MAP[plain_hash_val].copy())
|
|
||||||
return self
|
|
||||||
|
|
||||||
labelled_expression = tex_parser.get_labelled_expression()
|
if not self.use_plain_tex:
|
||||||
full_tex = self.get_tex_file_body(labelled_expression)
|
with display_during_execution(f"Writing \"{self.tex_string}\""):
|
||||||
hash_val = hash(full_tex)
|
labelled_svg_glyphs = self.tex_content_to_glyphs(
|
||||||
if hash_val in TEX_HASH_TO_MOB_MAP and not self.generate_plain_tex_file:
|
labelled_tex_content
|
||||||
self.add(*TEX_HASH_TO_MOB_MAP[hash_val].copy())
|
)
|
||||||
return self
|
glyph_labels = [
|
||||||
|
self.color_to_label(labelled_glyph.get_fill_color())
|
||||||
|
for labelled_glyph in labelled_svg_glyphs
|
||||||
|
]
|
||||||
|
mob = self.build_mobject(labelled_svg_glyphs, glyph_labels)
|
||||||
|
TEX_HASH_TO_MOB_MAP[hash_val] = mob
|
||||||
|
return mob
|
||||||
|
|
||||||
with display_during_execution(f"Writing \"{tex_string}\""):
|
with display_during_execution(f"Writing \"{self.tex_string}\""):
|
||||||
filename = tex_to_svg_file(full_tex)
|
labelled_svg_glyphs = self.tex_content_to_glyphs(
|
||||||
svg_mob = _LabelledTex(filename)
|
labelled_tex_content
|
||||||
self.add(*svg_mob.copy())
|
)
|
||||||
self.build_submobjects()
|
tex_content = self.get_tex_file_content(self.tex_string)
|
||||||
TEX_HASH_TO_MOB_MAP[hash_val] = self
|
svg_glyphs = self.tex_content_to_glyphs(tex_content)
|
||||||
if not self.generate_plain_tex_file:
|
glyph_labels = [
|
||||||
return self
|
self.color_to_label(labelled_glyph.get_fill_color())
|
||||||
|
for labelled_glyph in labelled_svg_glyphs
|
||||||
|
]
|
||||||
|
mob = self.build_mobject(svg_glyphs, glyph_labels)
|
||||||
|
TEX_HASH_TO_MOB_MAP[hash_val] = mob
|
||||||
|
return mob
|
||||||
|
|
||||||
with display_during_execution(f"Writing \"{tex_string}\""):
|
def get_tex_file_content(self, tex_string):
|
||||||
filename = tex_to_svg_file(plain_full_tex)
|
|
||||||
plain_svg_mob = _PlainTex(filename)
|
|
||||||
svg_mob = TEX_HASH_TO_MOB_MAP[hash_val]
|
|
||||||
for plain_submob, submob in zip(plain_svg_mob, svg_mob):
|
|
||||||
plain_submob.glyph_label = submob.glyph_label
|
|
||||||
self.add(*plain_svg_mob.copy())
|
|
||||||
self.build_submobjects()
|
|
||||||
TEX_HASH_TO_MOB_MAP[plain_hash_val] = self
|
|
||||||
return self
|
|
||||||
|
|
||||||
def get_tex_file_body(self, new_tex):
|
|
||||||
if self.tex_environment:
|
if self.tex_environment:
|
||||||
new_tex = "\n".join([
|
tex_string = "\n".join([
|
||||||
f"\\begin{{{self.tex_environment}}}",
|
f"\\begin{{{self.tex_environment}}}",
|
||||||
new_tex,
|
tex_string,
|
||||||
f"\\end{{{self.tex_environment}}}"
|
f"\\end{{{self.tex_environment}}}"
|
||||||
])
|
])
|
||||||
if self.alignment:
|
if self.alignment:
|
||||||
new_tex = "\n".join([self.alignment, new_tex])
|
tex_string = "\n".join([self.alignment, tex_string])
|
||||||
|
return tex_string
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def tex_content_to_glyphs(tex_content):
|
||||||
tex_config = get_tex_config()
|
tex_config = get_tex_config()
|
||||||
return tex_config["tex_body"].replace(
|
full_tex = tex_config["tex_body"].replace(
|
||||||
tex_config["text_to_replace"],
|
tex_config["text_to_replace"],
|
||||||
new_tex
|
tex_content
|
||||||
)
|
)
|
||||||
|
filename = tex_to_svg_file(full_tex)
|
||||||
|
return _TexSVG(filename)
|
||||||
|
|
||||||
def build_submobjects(self):
|
def build_mobject(self, svg_glyphs, glyph_labels):
|
||||||
if not self.submobjects:
|
if not svg_glyphs:
|
||||||
return
|
return VGroup()
|
||||||
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.
|
# Simply pack together adjacent mobjects with the same label.
|
||||||
new_submobjects = []
|
submobjects = []
|
||||||
def append_new_submobject(glyphs):
|
submob_labels = []
|
||||||
if glyphs:
|
|
||||||
submobject = VGroup(*glyphs)
|
|
||||||
submobject.submob_label = glyphs[0].glyph_label
|
|
||||||
new_submobjects.append(submobject)
|
|
||||||
|
|
||||||
new_glyphs = []
|
new_glyphs = []
|
||||||
current_glyph_label = 0
|
current_glyph_label = glyph_labels[0]
|
||||||
for submob in self.submobjects:
|
for glyph, label in zip(svg_glyphs, glyph_labels):
|
||||||
if submob.glyph_label == current_glyph_label:
|
if label == current_glyph_label:
|
||||||
new_glyphs.append(submob)
|
new_glyphs.append(glyph)
|
||||||
else:
|
else:
|
||||||
append_new_submobject(new_glyphs)
|
submobject = VGroup(*new_glyphs)
|
||||||
new_glyphs = [submob]
|
submob_labels.append(current_glyph_label)
|
||||||
current_glyph_label = submob.glyph_label
|
submobjects.append(submobject)
|
||||||
append_new_submobject(new_glyphs)
|
new_glyphs = [glyph]
|
||||||
self.set_submobjects(new_submobjects)
|
current_glyph_label = label
|
||||||
|
submobject = VGroup(*new_glyphs)
|
||||||
|
submob_labels.append(current_glyph_label)
|
||||||
|
submobjects.append(submobject)
|
||||||
|
|
||||||
def sort_scripts_in_tex_order(self):
|
indices = self.__parser.get_sorted_submob_indices(submob_labels)
|
||||||
# LaTeX always puts superscripts before subscripts.
|
rearranged_submobjects = [submobjects[index] for index in indices]
|
||||||
# This function sorts the submobjects of scripts in the order of tex given.
|
rearranged_labels = [submob_labels[index] for index in indices]
|
||||||
tex_spans_dict = self.tex_spans_dict
|
|
||||||
index_and_span_list = sorted([
|
|
||||||
(index, span_tuple)
|
|
||||||
for span_tuple, tex_span in tex_spans_dict.items()
|
|
||||||
if tex_span.script_type != 0
|
|
||||||
for index in span_tuple
|
|
||||||
])
|
|
||||||
|
|
||||||
switch_range_pairs = []
|
submob_tex_strings = self.__parser.get_submob_tex_strings(
|
||||||
for index_and_span_0, index_and_span_1 in _get_neighbouring_pairs(
|
rearranged_labels
|
||||||
index_and_span_list
|
)
|
||||||
|
for submob, label, submob_tex in zip(
|
||||||
|
rearranged_submobjects, rearranged_labels, submob_tex_strings
|
||||||
):
|
):
|
||||||
index_0, span_tuple_0 = index_and_span_0
|
submob.submob_label = label
|
||||||
index_1, span_tuple_1 = index_and_span_1
|
submob.tex_string = submob_tex
|
||||||
if index_0 != index_1:
|
|
||||||
continue
|
|
||||||
if not all([
|
|
||||||
tex_spans_dict[span_tuple_0].script_type == 1,
|
|
||||||
tex_spans_dict[span_tuple_1].script_type == 2
|
|
||||||
]):
|
|
||||||
continue
|
|
||||||
submob_range_0 = self.range_of_part(
|
|
||||||
self.get_part_by_span_tuples([span_tuple_0])
|
|
||||||
)
|
|
||||||
submob_range_1 = self.range_of_part(
|
|
||||||
self.get_part_by_span_tuples([span_tuple_1])
|
|
||||||
)
|
|
||||||
switch_range_pairs.append((submob_range_0, submob_range_1))
|
|
||||||
|
|
||||||
switch_range_pairs.sort(key=lambda pair: (pair[0].stop, -pair[0].start))
|
|
||||||
indices = list(range(len(self.submobjects)))
|
|
||||||
for submob_range_0, submob_range_1 in switch_range_pairs:
|
|
||||||
indices = [
|
|
||||||
*indices[: submob_range_1.start],
|
|
||||||
*indices[submob_range_0.start : submob_range_0.stop],
|
|
||||||
*indices[submob_range_1.stop : submob_range_0.start],
|
|
||||||
*indices[submob_range_1.start : submob_range_1.stop],
|
|
||||||
*indices[submob_range_0.stop :]
|
|
||||||
]
|
|
||||||
|
|
||||||
submobs = self.submobjects
|
|
||||||
self.set_submobjects([submobs[i] for i in indices])
|
|
||||||
|
|
||||||
def assign_submob_tex_strings(self):
|
|
||||||
# Not sure whether this is the best practice...
|
|
||||||
# This temporarily supports `TransformMatchingTex`.
|
|
||||||
tex_string = self.tex_string
|
|
||||||
tex_spans_dict = self.tex_spans_dict
|
|
||||||
# Use tex strings including "_", "^".
|
|
||||||
label_dict = {}
|
|
||||||
for span_tuple, tex_span in tex_spans_dict.items():
|
|
||||||
if tex_span.script_type != 0:
|
|
||||||
label_dict[tex_span.label] = span_tuple
|
|
||||||
else:
|
|
||||||
if tex_span.label not in label_dict:
|
|
||||||
label_dict[tex_span.label] = span_tuple
|
|
||||||
|
|
||||||
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_label, prev_label, next_label in zip(
|
|
||||||
curr_labels, prev_labels, next_labels
|
|
||||||
):
|
|
||||||
curr_span_tuple = label_dict[curr_label]
|
|
||||||
prev_span_tuple = label_dict[prev_label]
|
|
||||||
next_span_tuple = label_dict[next_label]
|
|
||||||
containing_labels = tex_spans_dict[curr_span_tuple].containing_labels
|
|
||||||
tex_string_spans.append([
|
|
||||||
prev_span_tuple[1] if prev_label in containing_labels else curr_span_tuple[0],
|
|
||||||
next_span_tuple[0] if next_label in containing_labels else curr_span_tuple[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.
|
# Support `get_tex()` method here.
|
||||||
submob.get_tex = MethodType(lambda inst: inst.tex_string, submob)
|
submob.get_tex = MethodType(lambda inst: inst.tex_string, submob)
|
||||||
|
return VGroup(*rearranged_submobjects)
|
||||||
|
|
||||||
def get_part_by_span_tuples(self, span_tuples):
|
def get_part_by_tex_spans(self, tex_spans):
|
||||||
tex_spans_dict = self.tex_spans_dict
|
labels = self.__parser.get_containing_labels_by_tex_spans(tex_spans)
|
||||||
labels = set(it.chain(*[
|
|
||||||
tex_spans_dict[span_tuple].containing_labels
|
|
||||||
for span_tuple in span_tuples
|
|
||||||
]))
|
|
||||||
return VGroup(*filter(
|
return VGroup(*filter(
|
||||||
lambda submob: submob.submob_label in labels,
|
lambda submob: submob.submob_label in labels,
|
||||||
self.submobjects
|
self.submobjects
|
||||||
))
|
))
|
||||||
|
|
||||||
def find_span_components_of_custom_span(self, custom_span_tuple, partial_result=[]):
|
def get_part_by_custom_span(self, custom_span):
|
||||||
span_begin, span_end = custom_span_tuple
|
tex_spans = self.__parser.find_span_components_of_custom_span(
|
||||||
if span_begin == span_end:
|
custom_span
|
||||||
return partial_result
|
)
|
||||||
next_begin_choices = sorted([
|
if tex_spans is None:
|
||||||
span_tuple[1]
|
tex = self.tex_string[slice(*custom_span)]
|
||||||
for span_tuple in self.tex_spans_dict.keys()
|
raise ValueError(f"Failed to match mobjects from tex: \"{tex}\"")
|
||||||
if span_tuple[0] == span_begin and span_tuple[1] <= span_end
|
return self.get_part_by_tex_spans(tex_spans)
|
||||||
], reverse=True)
|
|
||||||
for next_begin in next_begin_choices:
|
|
||||||
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_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}\"")
|
|
||||||
return self.get_part_by_span_tuples(span_tuples)
|
|
||||||
|
|
||||||
def get_parts_by_tex(self, tex):
|
def get_parts_by_tex(self, tex):
|
||||||
return VGroup(*[
|
return VGroup(*[
|
||||||
self.get_part_by_custom_span_tuple(match_obj.span())
|
self.get_part_by_custom_span(match_obj.span())
|
||||||
for match_obj in re.finditer(re.escape(tex), self.tex_string)
|
for match_obj in re.finditer(
|
||||||
|
re.escape(tex.strip()), self.tex_string
|
||||||
|
)
|
||||||
])
|
])
|
||||||
|
|
||||||
def get_part_by_tex(self, tex, index=0):
|
def get_part_by_tex(self, tex, index=0):
|
||||||
|
@ -525,16 +563,13 @@ class MTex(VMobject):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_color_by_tex_to_color_map(self, tex_to_color_map):
|
def set_color_by_tex_to_color_map(self, tex_to_color_map):
|
||||||
for tex, color in list(tex_to_color_map.items()):
|
for tex, color in tex_to_color_map.items():
|
||||||
try:
|
self.set_color_by_tex(tex, color)
|
||||||
self.set_color_by_tex(tex, color)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def indices_of_part(self, part):
|
def indices_of_part(self, part):
|
||||||
indices = [
|
indices = [
|
||||||
i for i, submob in enumerate(self.submobjects)
|
index for index, submob in enumerate(self.submobjects)
|
||||||
if submob in part
|
if submob in part
|
||||||
]
|
]
|
||||||
if not indices:
|
if not indices:
|
||||||
|
@ -545,42 +580,20 @@ class MTex(VMobject):
|
||||||
part = self.get_part_by_tex(tex, index=index)
|
part = self.get_part_by_tex(tex, index=index)
|
||||||
return self.indices_of_part(part)
|
return self.indices_of_part(part)
|
||||||
|
|
||||||
def indices_of_all_parts_by_tex(self, tex, index=0):
|
|
||||||
all_parts = self.get_parts_by_tex(tex)
|
|
||||||
return list(it.chain(*[
|
|
||||||
self.indices_of_part(part) for part in all_parts
|
|
||||||
]))
|
|
||||||
|
|
||||||
def range_of_part(self, part):
|
|
||||||
indices = self.indices_of_part(part)
|
|
||||||
return range(indices[0], indices[-1] + 1)
|
|
||||||
|
|
||||||
def range_of_part_by_tex(self, tex, index=0):
|
|
||||||
part = self.get_part_by_tex(tex, index=index)
|
|
||||||
return self.range_of_part(part)
|
|
||||||
|
|
||||||
def index_of_part(self, part):
|
|
||||||
return self.indices_of_part(part)[0]
|
|
||||||
|
|
||||||
def index_of_part_by_tex(self, tex, index=0):
|
|
||||||
part = self.get_part_by_tex(tex, index=index)
|
|
||||||
return self.index_of_part(part)
|
|
||||||
|
|
||||||
def get_tex(self):
|
def get_tex(self):
|
||||||
return self.tex_string
|
return self.tex_string
|
||||||
|
|
||||||
def get_all_isolated_substrings(self):
|
def get_submob_tex(self):
|
||||||
tex_string = self.tex_string
|
return [
|
||||||
return remove_list_redundancies([
|
submob.get_tex()
|
||||||
tex_string[slice(*span_tuple)]
|
for submob in self.submobjects
|
||||||
for span_tuple in self.tex_spans_dict.keys()
|
]
|
||||||
])
|
|
||||||
|
|
||||||
def list_tex_strings_of_submobjects(self):
|
def get_specified_substrings(self):
|
||||||
# Work with `index_labels()`.
|
return self.__parser.get_specified_substrings()
|
||||||
log.debug(f"Submobjects of \"{self.get_tex()}\":")
|
|
||||||
for i, submob in enumerate(self.submobjects):
|
def get_isolated_substrings(self):
|
||||||
log.debug(f"{i}: \"{submob.get_tex()}\"")
|
return self.__parser.get_isolated_substrings()
|
||||||
|
|
||||||
|
|
||||||
class MTexText(MTex):
|
class MTexText(MTex):
|
||||||
|
|
|
@ -1,102 +1,27 @@
|
||||||
import itertools as it
|
|
||||||
import re
|
|
||||||
import string
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import itertools as it
|
||||||
|
|
||||||
import cssselect2
|
import svgelements as se
|
||||||
from colour import web2hex
|
import numpy as np
|
||||||
from xml.etree import ElementTree
|
|
||||||
from tinycss2 import serialize as css_serialize
|
|
||||||
from tinycss2 import parse_stylesheet, parse_declaration_list
|
|
||||||
|
|
||||||
from manimlib.constants import ORIGIN, UP, DOWN, LEFT, RIGHT, IN
|
|
||||||
from manimlib.constants import DEGREES, PI
|
|
||||||
|
|
||||||
|
from manimlib.constants import RIGHT
|
||||||
from manimlib.mobject.geometry import Line
|
from manimlib.mobject.geometry import Line
|
||||||
from manimlib.mobject.geometry import Circle
|
from manimlib.mobject.geometry import Circle
|
||||||
|
from manimlib.mobject.geometry import Polygon
|
||||||
|
from manimlib.mobject.geometry import Polyline
|
||||||
from manimlib.mobject.geometry import Rectangle
|
from manimlib.mobject.geometry import Rectangle
|
||||||
from manimlib.mobject.geometry import RoundedRectangle
|
from manimlib.mobject.geometry import RoundedRectangle
|
||||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
|
||||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||||
from manimlib.utils.color import *
|
|
||||||
from manimlib.utils.config_ops import digest_config
|
from manimlib.utils.config_ops import digest_config
|
||||||
from manimlib.utils.directories import get_mobject_data_dir
|
from manimlib.utils.directories import get_mobject_data_dir
|
||||||
from manimlib.utils.images import get_full_vector_image_path
|
from manimlib.utils.images import get_full_vector_image_path
|
||||||
from manimlib.utils.simple_functions import clip
|
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_STYLE = {
|
def _convert_point_to_3d(x, y):
|
||||||
"fill": "black",
|
return np.array([x, y, 0.0])
|
||||||
"stroke": "none",
|
|
||||||
"fill-opacity": "1",
|
|
||||||
"stroke-opacity": "1",
|
|
||||||
"stroke-width": 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def cascade_element_style(element, inherited):
|
|
||||||
style = inherited.copy()
|
|
||||||
|
|
||||||
for attr in DEFAULT_STYLE:
|
|
||||||
if element.get(attr):
|
|
||||||
style[attr] = element.get(attr)
|
|
||||||
|
|
||||||
if element.get("style"):
|
|
||||||
declarations = parse_declaration_list(element.get("style"))
|
|
||||||
for declaration in declarations:
|
|
||||||
style[declaration.name] = css_serialize(declaration.value)
|
|
||||||
|
|
||||||
return style
|
|
||||||
|
|
||||||
|
|
||||||
def parse_color(color):
|
|
||||||
color = color.strip()
|
|
||||||
|
|
||||||
if color[0:3] == "rgb":
|
|
||||||
splits = color[4:-1].strip().split(",")
|
|
||||||
if splits[0].strip()[-1] == "%":
|
|
||||||
parsed_rgbs = [float(i.strip()[:-1]) / 100.0 for i in splits]
|
|
||||||
else:
|
|
||||||
parsed_rgbs = [int(i) / 255.0 for i in splits]
|
|
||||||
return rgb_to_hex(parsed_rgbs)
|
|
||||||
|
|
||||||
else:
|
|
||||||
return web2hex(color)
|
|
||||||
|
|
||||||
|
|
||||||
def fill_default_values(style, default_style):
|
|
||||||
default = DEFAULT_STYLE.copy()
|
|
||||||
default.update(default_style)
|
|
||||||
for attr in default:
|
|
||||||
if attr not in style:
|
|
||||||
style[attr] = default[attr]
|
|
||||||
|
|
||||||
|
|
||||||
def parse_style(style, default_style):
|
|
||||||
manim_style = {}
|
|
||||||
fill_default_values(style, default_style)
|
|
||||||
|
|
||||||
for key in ("fill-opacity", "stroke-opacity", "stroke-width"):
|
|
||||||
value = style[key]
|
|
||||||
if isinstance(value, str) and value.endswith("px"):
|
|
||||||
value = float(value[:-2]) * 0 # HACKY, need to fix
|
|
||||||
manim_style[key.replace("-", "_")] = float(value)
|
|
||||||
|
|
||||||
if style["fill"] == "none":
|
|
||||||
manim_style["fill_opacity"] = 0
|
|
||||||
else:
|
|
||||||
manim_style["fill_color"] = parse_color(style["fill"])
|
|
||||||
|
|
||||||
if style["stroke"] == "none":
|
|
||||||
manim_style["stroke_width"] = 0
|
|
||||||
if "fill_color" in manim_style:
|
|
||||||
manim_style["stroke_color"] = manim_style["fill_color"]
|
|
||||||
else:
|
|
||||||
manim_style["stroke_color"] = parse_color(style["stroke"])
|
|
||||||
|
|
||||||
return manim_style
|
|
||||||
|
|
||||||
|
|
||||||
class SVGMobject(VMobject):
|
class SVGMobject(VMobject):
|
||||||
|
@ -104,11 +29,15 @@ class SVGMobject(VMobject):
|
||||||
"should_center": True,
|
"should_center": True,
|
||||||
"height": 2,
|
"height": 2,
|
||||||
"width": None,
|
"width": None,
|
||||||
# Must be filled in in a subclass, or when called
|
# Must be filled in a subclass, or when called
|
||||||
"file_name": None,
|
"file_name": None,
|
||||||
"unpack_groups": True, # if False, creates a hierarchy of VGroups
|
"color": None,
|
||||||
"stroke_width": 0.0,
|
"opacity": None,
|
||||||
"fill_opacity": 1.0,
|
"fill_color": None,
|
||||||
|
"fill_opacity": None,
|
||||||
|
"stroke_width": None,
|
||||||
|
"stroke_color": None,
|
||||||
|
"stroke_opacity": None,
|
||||||
"path_string_config": {}
|
"path_string_config": {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,338 +59,232 @@ class SVGMobject(VMobject):
|
||||||
if self.width is not None:
|
if self.width is not None:
|
||||||
self.set_width(self.width)
|
self.set_width(self.width)
|
||||||
|
|
||||||
def init_colors(self, override=False):
|
def init_colors(self):
|
||||||
super().init_colors(override=override)
|
# Remove fill_color, fill_opacity,
|
||||||
|
# stroke_width, stroke_color, stroke_opacity
|
||||||
|
# as each submobject may have those values specified in svg file
|
||||||
|
self.set_stroke(background=self.draw_stroke_behind_fill)
|
||||||
|
self.set_gloss(self.gloss)
|
||||||
|
self.set_flat_stroke(self.flat_stroke)
|
||||||
|
return self
|
||||||
|
|
||||||
def init_points(self):
|
def init_points(self):
|
||||||
etree = ElementTree.parse(self.file_path)
|
with open(self.file_path, "r") as svg_file:
|
||||||
wrapper = cssselect2.ElementWrapper.from_xml_root(etree)
|
svg_string = svg_file.read()
|
||||||
svg = etree.getroot()
|
|
||||||
namespace = svg.tag.split("}")[0][1:]
|
|
||||||
self.ref_to_element = {}
|
|
||||||
self.css_matcher = cssselect2.Matcher()
|
|
||||||
|
|
||||||
for style in etree.findall(f"{{{namespace}}}style"):
|
# Create a temporary svg file to dump modified svg to be parsed
|
||||||
self.parse_css_style(style.text)
|
modified_svg_string = self.modify_svg_file(svg_string)
|
||||||
|
modified_file_path = self.file_path.replace(".svg", "_.svg")
|
||||||
|
with open(modified_file_path, "w") as modified_svg_file:
|
||||||
|
modified_svg_file.write(modified_svg_string)
|
||||||
|
|
||||||
mobjects = self.get_mobjects_from(wrapper, dict())
|
# `color` attribute handles `currentColor` keyword
|
||||||
if self.unpack_groups:
|
|
||||||
self.add(*mobjects)
|
|
||||||
else:
|
|
||||||
self.add(*mobjects[0].submobjects)
|
|
||||||
|
|
||||||
def get_mobjects_from(self, wrapper, style):
|
|
||||||
result = []
|
|
||||||
element = wrapper.etree_element
|
|
||||||
if not isinstance(element, ElementTree.Element):
|
|
||||||
return result
|
|
||||||
|
|
||||||
matches = self.css_matcher.match(wrapper)
|
|
||||||
if matches:
|
|
||||||
for match in matches:
|
|
||||||
_, _, _, css_style = match
|
|
||||||
style.update(css_style)
|
|
||||||
style = cascade_element_style(element, style)
|
|
||||||
|
|
||||||
tag = element.tag.split("}")[-1]
|
|
||||||
if tag == 'defs':
|
|
||||||
self.update_ref_to_element(wrapper, style)
|
|
||||||
elif tag in ['g', 'svg', 'symbol']:
|
|
||||||
result += it.chain(*(
|
|
||||||
self.get_mobjects_from(child, style)
|
|
||||||
for child in wrapper.iter_children()
|
|
||||||
))
|
|
||||||
elif tag == 'path':
|
|
||||||
result.append(self.path_string_to_mobject(
|
|
||||||
element.get('d'), style
|
|
||||||
))
|
|
||||||
elif tag == 'use':
|
|
||||||
result += self.use_to_mobjects(element, style)
|
|
||||||
elif tag == 'line':
|
|
||||||
result.append(self.line_to_mobject(element, style))
|
|
||||||
elif tag == 'rect':
|
|
||||||
result.append(self.rect_to_mobject(element, style))
|
|
||||||
elif tag == 'circle':
|
|
||||||
result.append(self.circle_to_mobject(element, style))
|
|
||||||
elif tag == 'ellipse':
|
|
||||||
result.append(self.ellipse_to_mobject(element, style))
|
|
||||||
elif tag in ['polygon', 'polyline']:
|
|
||||||
result.append(self.polygon_to_mobject(element, style))
|
|
||||||
elif tag == 'style':
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
log.warning(f"Unsupported element type: {tag}")
|
|
||||||
pass # TODO, support <text> tag
|
|
||||||
result = [m for m in result if m is not None]
|
|
||||||
self.handle_transforms(element, VGroup(*result))
|
|
||||||
if len(result) > 1 and not self.unpack_groups:
|
|
||||||
result = [VGroup(*result)]
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def generate_default_style(self):
|
|
||||||
style = {
|
|
||||||
"fill-opacity": self.fill_opacity,
|
|
||||||
"stroke-width": self.stroke_width,
|
|
||||||
"stroke-opacity": self.stroke_opacity,
|
|
||||||
}
|
|
||||||
if self.color:
|
|
||||||
style["fill"] = style["stroke"] = self.color
|
|
||||||
if self.fill_color:
|
if self.fill_color:
|
||||||
style["fill"] = self.fill_color
|
color = self.fill_color
|
||||||
if self.stroke_color:
|
elif self.color:
|
||||||
style["stroke"] = self.stroke_color
|
color = self.color
|
||||||
return style
|
else:
|
||||||
|
color = "black"
|
||||||
def parse_css_style(self, css):
|
shapes = se.SVG.parse(
|
||||||
rules = parse_stylesheet(css, True, True)
|
modified_file_path,
|
||||||
for rule in rules:
|
color=color
|
||||||
selectors = cssselect2.compile_selector_list(rule.prelude)
|
|
||||||
declarations = parse_declaration_list(rule.content)
|
|
||||||
style = {
|
|
||||||
declaration.name: css_serialize(declaration.value)
|
|
||||||
for declaration in declarations
|
|
||||||
if declaration.name in DEFAULT_STYLE
|
|
||||||
}
|
|
||||||
payload = style
|
|
||||||
for selector in selectors:
|
|
||||||
self.css_matcher.add_selector(selector, payload)
|
|
||||||
|
|
||||||
def path_string_to_mobject(self, path_string, style):
|
|
||||||
return VMobjectFromSVGPathstring(
|
|
||||||
path_string,
|
|
||||||
**self.path_string_config,
|
|
||||||
**parse_style(style, self.generate_default_style()),
|
|
||||||
)
|
)
|
||||||
|
os.remove(modified_file_path)
|
||||||
|
|
||||||
def use_to_mobjects(self, use_element, local_style):
|
mobjects = self.get_mobjects_from(shapes)
|
||||||
# Remove initial "#" character
|
self.add(*mobjects)
|
||||||
ref = use_element.get(r"{http://www.w3.org/1999/xlink}href")[1:]
|
self.flip(RIGHT) # Flip y
|
||||||
if ref not in self.ref_to_element:
|
self.scale(0.75)
|
||||||
log.warning(f"{ref} not recognized")
|
|
||||||
return VGroup()
|
|
||||||
def_element, def_style = self.ref_to_element[ref]
|
|
||||||
style = local_style.copy()
|
|
||||||
style.update(def_style)
|
|
||||||
return self.get_mobjects_from(def_element, style)
|
|
||||||
|
|
||||||
def attribute_to_float(self, attr):
|
def modify_svg_file(self, svg_string):
|
||||||
stripped_attr = "".join([
|
# svgelements cannot handle em, ex units
|
||||||
char for char in attr
|
# Convert them using 1em = 16px, 1ex = 0.5em = 8px
|
||||||
if char in string.digits + "." + "-"
|
def convert_unit(match_obj):
|
||||||
|
number = float(match_obj.group(1))
|
||||||
|
unit = match_obj.group(2)
|
||||||
|
factor = 16 if unit == "em" else 8
|
||||||
|
return str(number * factor) + "px"
|
||||||
|
|
||||||
|
number_pattern = r"([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?)(ex|em)(?![a-zA-Z])"
|
||||||
|
result = re.sub(number_pattern, convert_unit, svg_string)
|
||||||
|
|
||||||
|
# Add a group tag to set style from configuration
|
||||||
|
style_dict = self.generate_context_values_from_config()
|
||||||
|
group_tag_begin = "<g " + " ".join([
|
||||||
|
f"{k}=\"{v}\""
|
||||||
|
for k, v in style_dict.items()
|
||||||
|
]) + ">"
|
||||||
|
group_tag_end = "</g>"
|
||||||
|
begin_insert_index = re.search(r"<svg[\s\S]*?>", result).end()
|
||||||
|
end_insert_index = re.search(r"[\s\S]*(</svg\s*>)", result).start(1)
|
||||||
|
result = "".join([
|
||||||
|
result[:begin_insert_index],
|
||||||
|
group_tag_begin,
|
||||||
|
result[begin_insert_index:end_insert_index],
|
||||||
|
group_tag_end,
|
||||||
|
result[end_insert_index:]
|
||||||
])
|
])
|
||||||
return float(stripped_attr)
|
|
||||||
|
|
||||||
def polygon_to_mobject(self, polygon_element, style):
|
|
||||||
path_string = polygon_element.get("points")
|
|
||||||
for digit in string.digits:
|
|
||||||
path_string = path_string.replace(f" {digit}", f"L {digit}")
|
|
||||||
path_string = path_string.replace("L", "M", 1)
|
|
||||||
return self.path_string_to_mobject(path_string, style)
|
|
||||||
|
|
||||||
def circle_to_mobject(self, circle_element, style):
|
|
||||||
x, y, r = (
|
|
||||||
self.attribute_to_float(circle_element.get(key, "0.0"))
|
|
||||||
for key in ("cx", "cy", "r")
|
|
||||||
)
|
|
||||||
return Circle(
|
|
||||||
radius=r,
|
|
||||||
**parse_style(style, self.generate_default_style())
|
|
||||||
).shift(x * RIGHT + y * DOWN)
|
|
||||||
|
|
||||||
def ellipse_to_mobject(self, circle_element, style):
|
|
||||||
x, y, rx, ry = (
|
|
||||||
self.attribute_to_float(circle_element.get(key, "0.0"))
|
|
||||||
for key in ("cx", "cy", "rx", "ry")
|
|
||||||
)
|
|
||||||
result = Circle(**parse_style(style, self.generate_default_style()))
|
|
||||||
result.stretch(rx, 0)
|
|
||||||
result.stretch(ry, 1)
|
|
||||||
result.shift(x * RIGHT + y * DOWN)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def rect_to_mobject(self, rect_element, style):
|
def generate_context_values_from_config(self):
|
||||||
stroke_width = rect_element.get("stroke-width", "")
|
result = {}
|
||||||
corner_radius = rect_element.get("rx", "")
|
if self.stroke_width is not None:
|
||||||
|
result["stroke-width"] = self.stroke_width
|
||||||
|
if self.color is not None:
|
||||||
|
result["fill"] = result["stroke"] = self.color
|
||||||
|
if self.fill_color is not None:
|
||||||
|
result["fill"] = self.fill_color
|
||||||
|
if self.stroke_color is not None:
|
||||||
|
result["stroke"] = self.stroke_color
|
||||||
|
if self.opacity is not None:
|
||||||
|
result["fill-opacity"] = result["stroke-opacity"] = self.opacity
|
||||||
|
if self.fill_opacity is not None:
|
||||||
|
result["fill-opacity"] = self.fill_opacity
|
||||||
|
if self.stroke_opacity is not None:
|
||||||
|
result["stroke-opacity"] = self.stroke_opacity
|
||||||
|
return result
|
||||||
|
|
||||||
if stroke_width in ["", "none", "0"]:
|
def get_mobjects_from(self, shape):
|
||||||
stroke_width = 0
|
if isinstance(shape, se.Group):
|
||||||
|
return list(it.chain(*(
|
||||||
|
self.get_mobjects_from(child)
|
||||||
|
for child in shape
|
||||||
|
)))
|
||||||
|
|
||||||
if corner_radius in ["", "0", "none"]:
|
mob = self.get_mobject_from(shape)
|
||||||
corner_radius = 0
|
if mob is None:
|
||||||
|
return []
|
||||||
|
|
||||||
corner_radius = float(corner_radius)
|
if isinstance(shape, se.Transformable) and shape.apply:
|
||||||
|
self.handle_transform(mob, shape.transform)
|
||||||
|
return [mob]
|
||||||
|
|
||||||
parsed_style = parse_style(style, self.generate_default_style())
|
@staticmethod
|
||||||
parsed_style["stroke_width"] = stroke_width
|
def handle_transform(mob, matrix):
|
||||||
|
mat = np.array([
|
||||||
|
[matrix.a, matrix.c],
|
||||||
|
[matrix.b, matrix.d]
|
||||||
|
])
|
||||||
|
vec = np.array([matrix.e, matrix.f, 0.0])
|
||||||
|
mob.apply_matrix(mat)
|
||||||
|
mob.shift(vec)
|
||||||
|
return mob
|
||||||
|
|
||||||
if corner_radius == 0:
|
def get_mobject_from(self, shape):
|
||||||
|
shape_class_to_func_map = {
|
||||||
|
se.Path: self.path_to_mobject,
|
||||||
|
se.SimpleLine: self.line_to_mobject,
|
||||||
|
se.Rect: self.rect_to_mobject,
|
||||||
|
se.Circle: self.circle_to_mobject,
|
||||||
|
se.Ellipse: self.ellipse_to_mobject,
|
||||||
|
se.Polygon: self.polygon_to_mobject,
|
||||||
|
se.Polyline: self.polyline_to_mobject,
|
||||||
|
# se.Text: self.text_to_mobject, # TODO
|
||||||
|
}
|
||||||
|
for shape_class, func in shape_class_to_func_map.items():
|
||||||
|
if isinstance(shape, shape_class):
|
||||||
|
mob = func(shape)
|
||||||
|
self.apply_style_to_mobject(mob, shape)
|
||||||
|
return mob
|
||||||
|
|
||||||
|
shape_class_name = shape.__class__.__name__
|
||||||
|
if shape_class_name != "SVGElement":
|
||||||
|
log.warning(f"Unsupported element type: {shape_class_name}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def apply_style_to_mobject(mob, shape):
|
||||||
|
mob.set_style(
|
||||||
|
stroke_width=shape.stroke_width,
|
||||||
|
stroke_color=shape.stroke.hex,
|
||||||
|
stroke_opacity=shape.stroke.opacity,
|
||||||
|
fill_color=shape.fill.hex,
|
||||||
|
fill_opacity=shape.fill.opacity
|
||||||
|
)
|
||||||
|
return mob
|
||||||
|
|
||||||
|
def path_to_mobject(self, path):
|
||||||
|
return VMobjectFromSVGPath(path, **self.path_string_config)
|
||||||
|
|
||||||
|
def line_to_mobject(self, line):
|
||||||
|
return Line(
|
||||||
|
start=_convert_point_to_3d(line.x1, line.y1),
|
||||||
|
end=_convert_point_to_3d(line.x2, line.y2)
|
||||||
|
)
|
||||||
|
|
||||||
|
def rect_to_mobject(self, rect):
|
||||||
|
if rect.rx == 0 or rect.ry == 0:
|
||||||
mob = Rectangle(
|
mob = Rectangle(
|
||||||
width=self.attribute_to_float(
|
width=rect.width,
|
||||||
rect_element.get("width", "")
|
height=rect.height,
|
||||||
),
|
|
||||||
height=self.attribute_to_float(
|
|
||||||
rect_element.get("height", "")
|
|
||||||
),
|
|
||||||
**parsed_style,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
mob = RoundedRectangle(
|
mob = RoundedRectangle(
|
||||||
width=self.attribute_to_float(
|
width=rect.width,
|
||||||
rect_element.get("width", "")
|
height=rect.height * rect.rx / rect.ry,
|
||||||
),
|
corner_radius=rect.rx
|
||||||
height=self.attribute_to_float(
|
|
||||||
rect_element.get("height", "")
|
|
||||||
),
|
|
||||||
corner_radius=corner_radius,
|
|
||||||
**parsed_style
|
|
||||||
)
|
)
|
||||||
|
mob.stretch_to_fit_height(rect.height)
|
||||||
mob.shift(mob.get_center() - mob.get_corner(UP + LEFT))
|
mob.shift(_convert_point_to_3d(
|
||||||
|
rect.x + rect.width / 2,
|
||||||
|
rect.y + rect.height / 2
|
||||||
|
))
|
||||||
return mob
|
return mob
|
||||||
|
|
||||||
def line_to_mobject(self, line_element, style):
|
def circle_to_mobject(self, circle):
|
||||||
x1, y1, x2, y2 = (
|
# svgelements supports `rx` & `ry` but `r`
|
||||||
self.attribute_to_float(line_element.get(key, "0.0"))
|
mob = Circle(radius=circle.rx)
|
||||||
for key in ("x1", "y1", "x2", "y2")
|
mob.shift(_convert_point_to_3d(
|
||||||
)
|
circle.cx, circle.cy
|
||||||
return Line(
|
))
|
||||||
[x1, -y1, 0], [x2, -y2, 0],
|
return mob
|
||||||
**parse_style(style, self.generate_default_style())
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_transforms(self, element, mobject):
|
def ellipse_to_mobject(self, ellipse):
|
||||||
x, y = (
|
mob = Circle(radius=ellipse.rx)
|
||||||
self.attribute_to_float(element.get(key, "0.0"))
|
mob.stretch_to_fit_height(2 * ellipse.ry)
|
||||||
for key in ("x", "y")
|
mob.shift(_convert_point_to_3d(
|
||||||
)
|
ellipse.cx, ellipse.cy
|
||||||
mobject.shift(x * RIGHT + y * DOWN)
|
))
|
||||||
|
return mob
|
||||||
|
|
||||||
transform_names = [
|
def polygon_to_mobject(self, polygon):
|
||||||
"matrix",
|
points = [
|
||||||
"translate", "translateX", "translateY",
|
_convert_point_to_3d(*point)
|
||||||
"scale", "scaleX", "scaleY",
|
for point in polygon
|
||||||
"rotate",
|
|
||||||
"skewX", "skewY"
|
|
||||||
]
|
]
|
||||||
transform_pattern = re.compile("|".join([x + r"[^)]*\)" for x in transform_names]))
|
return Polygon(*points)
|
||||||
number_pattern = re.compile(r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?")
|
|
||||||
transforms = transform_pattern.findall(element.get("transform", ""))[::-1]
|
|
||||||
|
|
||||||
for transform in transforms:
|
def polyline_to_mobject(self, polyline):
|
||||||
op_name, op_args = transform.split("(")
|
points = [
|
||||||
op_name = op_name.strip()
|
_convert_point_to_3d(*point)
|
||||||
op_args = [float(x) for x in number_pattern.findall(op_args)]
|
for point in polyline
|
||||||
|
]
|
||||||
|
return Polyline(*points)
|
||||||
|
|
||||||
if op_name == "matrix":
|
def text_to_mobject(self, text):
|
||||||
self._handle_matrix_transform(mobject, op_name, op_args)
|
pass
|
||||||
elif op_name.startswith("translate"):
|
|
||||||
self._handle_translate_transform(mobject, op_name, op_args)
|
|
||||||
elif op_name.startswith("scale"):
|
|
||||||
self._handle_scale_transform(mobject, op_name, op_args)
|
|
||||||
elif op_name == "rotate":
|
|
||||||
self._handle_rotate_transform(mobject, op_name, op_args)
|
|
||||||
elif op_name.startswith("skew"):
|
|
||||||
self._handle_skew_transform(mobject, op_name, op_args)
|
|
||||||
|
|
||||||
def _handle_matrix_transform(self, mobject, op_name, op_args):
|
|
||||||
transform = np.array(op_args).reshape([3, 2])
|
|
||||||
x = transform[2][0]
|
|
||||||
y = -transform[2][1]
|
|
||||||
matrix = np.identity(self.dim)
|
|
||||||
matrix[:2, :2] = transform[:2, :]
|
|
||||||
matrix[1] *= -1
|
|
||||||
matrix[:, 1] *= -1
|
|
||||||
for mob in mobject.family_members_with_points():
|
|
||||||
mob.apply_matrix(matrix.T)
|
|
||||||
mobject.shift(x * RIGHT + y * UP)
|
|
||||||
|
|
||||||
def _handle_translate_transform(self, mobject, op_name, op_args):
|
|
||||||
if op_name.endswith("X"):
|
|
||||||
x, y = op_args[0], 0
|
|
||||||
elif op_name.endswith("Y"):
|
|
||||||
x, y = 0, op_args[0]
|
|
||||||
else:
|
|
||||||
x, y = op_args
|
|
||||||
mobject.shift(x * RIGHT + y * DOWN)
|
|
||||||
|
|
||||||
def _handle_scale_transform(self, mobject, op_name, op_args):
|
|
||||||
if op_name.endswith("X"):
|
|
||||||
sx, sy = op_args[0], 1
|
|
||||||
elif op_name.endswith("Y"):
|
|
||||||
sx, sy = 1, op_args[0]
|
|
||||||
elif len(op_args) == 2:
|
|
||||||
sx, sy = op_args
|
|
||||||
else:
|
|
||||||
sx = sy = op_args[0]
|
|
||||||
if sx < 0:
|
|
||||||
mobject.flip(UP)
|
|
||||||
sx = -sx
|
|
||||||
if sy < 0:
|
|
||||||
mobject.flip(RIGHT)
|
|
||||||
sy = -sy
|
|
||||||
mobject.scale(np.array([sx, sy, 1]), about_point=ORIGIN)
|
|
||||||
|
|
||||||
def _handle_rotate_transform(self, mobject, op_name, op_args):
|
|
||||||
if len(op_args) == 1:
|
|
||||||
mobject.rotate(op_args[0] * DEGREES, axis=IN, about_point=ORIGIN)
|
|
||||||
else:
|
|
||||||
deg, x, y = op_args
|
|
||||||
mobject.rotate(deg * DEGREES, axis=IN, about_point=np.array([x, y, 0]))
|
|
||||||
|
|
||||||
def _handle_skew_transform(self, mobject, op_name, op_args):
|
|
||||||
rad = op_args[0] * DEGREES
|
|
||||||
if op_name == "skewX":
|
|
||||||
tana = np.tan(rad)
|
|
||||||
self._handle_matrix_transform(mobject, None, [1., 0., tana, 1., 0., 0.])
|
|
||||||
elif op_name == "skewY":
|
|
||||||
tana = np.tan(rad)
|
|
||||||
self._handle_matrix_transform(mobject, None, [1., tana, 0., 1., 0., 0.])
|
|
||||||
|
|
||||||
def flatten(self, input_list):
|
|
||||||
output_list = []
|
|
||||||
for i in input_list:
|
|
||||||
if isinstance(i, list):
|
|
||||||
output_list.extend(self.flatten(i))
|
|
||||||
else:
|
|
||||||
output_list.append(i)
|
|
||||||
return output_list
|
|
||||||
|
|
||||||
def get_all_childWrappers_have_id(self, wrapper):
|
|
||||||
all_childWrappers_have_id = []
|
|
||||||
element = wrapper.etree_element
|
|
||||||
if not isinstance(element, ElementTree.Element):
|
|
||||||
return
|
|
||||||
if element.get('id'):
|
|
||||||
return [wrapper]
|
|
||||||
for e in wrapper.iter_children():
|
|
||||||
all_childWrappers_have_id.append(self.get_all_childWrappers_have_id(e))
|
|
||||||
return self.flatten([e for e in all_childWrappers_have_id if e])
|
|
||||||
|
|
||||||
def update_ref_to_element(self, wrapper, style):
|
|
||||||
new_refs = {
|
|
||||||
e.etree_element.get('id', ''): (e, style)
|
|
||||||
for e in self.get_all_childWrappers_have_id(wrapper)
|
|
||||||
}
|
|
||||||
self.ref_to_element.update(new_refs)
|
|
||||||
|
|
||||||
|
|
||||||
class VMobjectFromSVGPathstring(VMobject):
|
class VMobjectFromSVGPath(VMobject):
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"long_lines": False,
|
"long_lines": False,
|
||||||
"should_subdivide_sharp_curves": False,
|
"should_subdivide_sharp_curves": False,
|
||||||
"should_remove_null_curves": False,
|
"should_remove_null_curves": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, path_string, **kwargs):
|
def __init__(self, path_obj, **kwargs):
|
||||||
self.path_string = path_string
|
# Get rid of arcs
|
||||||
|
path_obj.approximate_arcs_with_quads()
|
||||||
|
self.path_obj = path_obj
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def init_points(self):
|
def init_points(self):
|
||||||
# After a given svg_path has been converted into points, the result
|
# After a given svg_path has been converted into points, the result
|
||||||
# will be saved to a file so that future calls for the same path
|
# will be saved to a file so that future calls for the same path
|
||||||
# don't need to retrace the same computation.
|
# don't need to retrace the same computation.
|
||||||
hasher = hashlib.sha256(self.path_string.encode())
|
path_string = self.path_obj.d()
|
||||||
|
hasher = hashlib.sha256(path_string.encode())
|
||||||
path_hash = hasher.hexdigest()[:16]
|
path_hash = hasher.hexdigest()[:16]
|
||||||
points_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_points.npy")
|
points_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_points.npy")
|
||||||
tris_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_tris.npy")
|
tris_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_tris.npy")
|
||||||
|
@ -478,239 +301,23 @@ class VMobjectFromSVGPathstring(VMobject):
|
||||||
if self.should_remove_null_curves:
|
if self.should_remove_null_curves:
|
||||||
# Get rid of any null curves
|
# Get rid of any null curves
|
||||||
self.set_points(self.get_points_without_null_curves())
|
self.set_points(self.get_points_without_null_curves())
|
||||||
# SVG treats y-coordinate differently
|
|
||||||
self.stretch(-1, 1, about_point=ORIGIN)
|
|
||||||
# Save to a file for future use
|
# Save to a file for future use
|
||||||
np.save(points_filepath, self.get_points())
|
np.save(points_filepath, self.get_points())
|
||||||
np.save(tris_filepath, self.get_triangulation())
|
np.save(tris_filepath, self.get_triangulation())
|
||||||
|
|
||||||
def get_commands_and_coord_strings(self):
|
|
||||||
all_commands = list(self.get_command_to_function_map().keys())
|
|
||||||
all_commands += [c.lower() for c in all_commands]
|
|
||||||
pattern = "[{}]".format("".join(all_commands))
|
|
||||||
return zip(
|
|
||||||
re.findall(pattern, self.path_string),
|
|
||||||
re.split(pattern, self.path_string)[1:]
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_commands(self):
|
def handle_commands(self):
|
||||||
relative_point = ORIGIN
|
segment_class_to_func_map = {
|
||||||
for command, coord_string in self.get_commands_and_coord_strings():
|
se.Move: (self.start_new_path, ("end",)),
|
||||||
func, number_types_str = self.command_to_function(command)
|
se.Close: (self.close_path, ()),
|
||||||
upper_command = command.upper()
|
se.Line: (self.add_line_to, ("end",)),
|
||||||
if upper_command == "Z":
|
se.QuadraticBezier: (self.add_quadratic_bezier_curve_to, ("control", "end")),
|
||||||
func() # `close_path` takes no arguments
|
se.CubicBezier: (self.add_cubic_bezier_curve_to, ("control1", "control2", "end"))
|
||||||
relative_point = self.get_last_point()
|
|
||||||
continue
|
|
||||||
|
|
||||||
number_types = np.array(list(number_types_str))
|
|
||||||
n_numbers = len(number_types_str)
|
|
||||||
number_list = _PathStringParser(coord_string, number_types_str).args
|
|
||||||
number_groups = np.array(number_list).reshape((-1, n_numbers))
|
|
||||||
|
|
||||||
for ind, numbers in enumerate(number_groups):
|
|
||||||
if command.islower():
|
|
||||||
# Treat it as a relative command
|
|
||||||
numbers[number_types == "x"] += relative_point[0]
|
|
||||||
numbers[number_types == "y"] += relative_point[1]
|
|
||||||
|
|
||||||
if upper_command == "A":
|
|
||||||
args = [*numbers[:5], np.array([*numbers[5:7], 0.0])]
|
|
||||||
elif upper_command == "H":
|
|
||||||
args = [np.array([numbers[0], relative_point[1], 0.0])]
|
|
||||||
elif upper_command == "V":
|
|
||||||
args = [np.array([relative_point[0], numbers[0], 0.0])]
|
|
||||||
else:
|
|
||||||
args = list(np.hstack((
|
|
||||||
numbers.reshape((-1, 2)), np.zeros((n_numbers // 2, 1))
|
|
||||||
)))
|
|
||||||
if upper_command == "M" and ind != 0:
|
|
||||||
# M x1 y1 x2 y2 is equal to M x1 y1 L x2 y2
|
|
||||||
func, _ = self.command_to_function("L")
|
|
||||||
func(*args)
|
|
||||||
relative_point = self.get_last_point()
|
|
||||||
|
|
||||||
def add_elliptical_arc_to(self, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, point):
|
|
||||||
def close_to_zero(a, threshold=1e-5):
|
|
||||||
return abs(a) < threshold
|
|
||||||
|
|
||||||
def solve_2d_linear_equation(a, b, c):
|
|
||||||
"""
|
|
||||||
Using Crammer's rule to solve the linear equation `[a b]x = c`
|
|
||||||
where `a`, `b` and `c` are all 2d vectors.
|
|
||||||
"""
|
|
||||||
def det(a, b):
|
|
||||||
return a[0] * b[1] - a[1] * b[0]
|
|
||||||
d = det(a, b)
|
|
||||||
if close_to_zero(d):
|
|
||||||
raise Exception("Cannot handle 0 determinant.")
|
|
||||||
return [det(c, b) / d, det(a, c) / d]
|
|
||||||
|
|
||||||
def get_arc_center_and_angles(x0, y0, rx, ry, phi, large_arc_flag, sweep_flag, x1, y1):
|
|
||||||
"""
|
|
||||||
The parameter functions of an ellipse rotated `phi` radians counterclockwise is (on `alpha`):
|
|
||||||
x = cx + rx * cos(alpha) * cos(phi) + ry * sin(alpha) * sin(phi),
|
|
||||||
y = cy + rx * cos(alpha) * sin(phi) - ry * sin(alpha) * cos(phi).
|
|
||||||
Now we have two points sitting on the ellipse: `(x0, y0)`, `(x1, y1)`, corresponding to 4 equations,
|
|
||||||
and we want to hunt for 4 variables: `cx`, `cy`, `alpha0` and `alpha_1`.
|
|
||||||
Let `d_alpha = alpha1 - alpha0`, then:
|
|
||||||
if `sweep_flag = 0` and `large_arc_flag = 1`, then `PI <= d_alpha < 2 * PI`;
|
|
||||||
if `sweep_flag = 0` and `large_arc_flag = 0`, then `0 < d_alpha <= PI`;
|
|
||||||
if `sweep_flag = 1` and `large_arc_flag = 0`, then `-PI <= d_alpha < 0`;
|
|
||||||
if `sweep_flag = 1` and `large_arc_flag = 1`, then `-2 * PI < d_alpha <= -PI`.
|
|
||||||
"""
|
|
||||||
xd = x1 - x0
|
|
||||||
yd = y1 - y0
|
|
||||||
if close_to_zero(xd) and close_to_zero(yd):
|
|
||||||
raise Exception("Cannot find arc center since the start point and the end point meet.")
|
|
||||||
# Find `p = cos(alpha1) - cos(alpha0)`, `q = sin(alpha1) - sin(alpha0)`
|
|
||||||
eq0 = [rx * np.cos(phi), ry * np.sin(phi), xd]
|
|
||||||
eq1 = [rx * np.sin(phi), -ry * np.cos(phi), yd]
|
|
||||||
p, q = solve_2d_linear_equation(*zip(eq0, eq1))
|
|
||||||
# Find `s = (alpha1 - alpha0) / 2`, `t = (alpha1 + alpha0) / 2`
|
|
||||||
# If `sin(s) = 0`, this requires `p = q = 0`,
|
|
||||||
# implying `xd = yd = 0`, which is impossible.
|
|
||||||
sin_s = (p ** 2 + q ** 2) ** 0.5 / 2
|
|
||||||
if sweep_flag:
|
|
||||||
sin_s = -sin_s
|
|
||||||
sin_s = clip(sin_s, -1, 1)
|
|
||||||
s = np.arcsin(sin_s)
|
|
||||||
if large_arc_flag:
|
|
||||||
if not sweep_flag:
|
|
||||||
s = PI - s
|
|
||||||
else:
|
|
||||||
s = -PI - s
|
|
||||||
sin_t = -p / (2 * sin_s)
|
|
||||||
cos_t = q / (2 * sin_s)
|
|
||||||
cos_t = clip(cos_t, -1, 1)
|
|
||||||
t = np.arccos(cos_t)
|
|
||||||
if sin_t <= 0:
|
|
||||||
t = -t
|
|
||||||
# We can make sure `0 < abs(s) < PI`, `-PI <= t < PI`.
|
|
||||||
alpha0 = t - s
|
|
||||||
alpha_1 = t + s
|
|
||||||
cx = x0 - rx * np.cos(alpha0) * np.cos(phi) - ry * np.sin(alpha0) * np.sin(phi)
|
|
||||||
cy = y0 - rx * np.cos(alpha0) * np.sin(phi) + ry * np.sin(alpha0) * np.cos(phi)
|
|
||||||
return cx, cy, alpha0, alpha_1
|
|
||||||
|
|
||||||
def get_point_on_ellipse(cx, cy, rx, ry, phi, angle):
|
|
||||||
return np.array([
|
|
||||||
cx + rx * np.cos(angle) * np.cos(phi) + ry * np.sin(angle) * np.sin(phi),
|
|
||||||
cy + rx * np.cos(angle) * np.sin(phi) - ry * np.sin(angle) * np.cos(phi),
|
|
||||||
0
|
|
||||||
])
|
|
||||||
|
|
||||||
def convert_elliptical_arc_to_quadratic_bezier_curve(
|
|
||||||
cx, cy, rx, ry, phi, start_angle, end_angle, n_components=8
|
|
||||||
):
|
|
||||||
theta = (end_angle - start_angle) / n_components / 2
|
|
||||||
handles = np.array([
|
|
||||||
get_point_on_ellipse(cx, cy, rx / np.cos(theta), ry / np.cos(theta), phi, a)
|
|
||||||
for a in np.linspace(
|
|
||||||
start_angle + theta,
|
|
||||||
end_angle - theta,
|
|
||||||
n_components,
|
|
||||||
)
|
|
||||||
])
|
|
||||||
anchors = np.array([
|
|
||||||
get_point_on_ellipse(cx, cy, rx, ry, phi, a)
|
|
||||||
for a in np.linspace(
|
|
||||||
start_angle + theta * 2,
|
|
||||||
end_angle,
|
|
||||||
n_components,
|
|
||||||
)
|
|
||||||
])
|
|
||||||
return handles, anchors
|
|
||||||
|
|
||||||
phi = x_axis_rotation * DEGREES
|
|
||||||
x0, y0 = self.get_last_point()[:2]
|
|
||||||
cx, cy, start_angle, end_angle = get_arc_center_and_angles(
|
|
||||||
x0, y0, rx, ry, phi, large_arc_flag, sweep_flag, point[0], point[1]
|
|
||||||
)
|
|
||||||
handles, anchors = convert_elliptical_arc_to_quadratic_bezier_curve(
|
|
||||||
cx, cy, rx, ry, phi, start_angle, end_angle
|
|
||||||
)
|
|
||||||
for handle, anchor in zip(handles, anchors):
|
|
||||||
self.add_quadratic_bezier_curve_to(handle, anchor)
|
|
||||||
|
|
||||||
def command_to_function(self, command):
|
|
||||||
return self.get_command_to_function_map()[command.upper()]
|
|
||||||
|
|
||||||
def get_command_to_function_map(self):
|
|
||||||
"""
|
|
||||||
Associates svg command to VMobject function, and
|
|
||||||
the types of arguments it takes in
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"M": (self.start_new_path, "xy"),
|
|
||||||
"L": (self.add_line_to, "xy"),
|
|
||||||
"H": (self.add_line_to, "x"),
|
|
||||||
"V": (self.add_line_to, "y"),
|
|
||||||
"C": (self.add_cubic_bezier_curve_to, "xyxyxy"),
|
|
||||||
"S": (self.add_smooth_cubic_curve_to, "xyxy"),
|
|
||||||
"Q": (self.add_quadratic_bezier_curve_to, "xyxy"),
|
|
||||||
"T": (self.add_smooth_curve_to, "xy"),
|
|
||||||
"A": (self.add_elliptical_arc_to, "uuaffxy"),
|
|
||||||
"Z": (self.close_path, ""),
|
|
||||||
}
|
}
|
||||||
|
for segment in self.path_obj:
|
||||||
def get_original_path_string(self):
|
segment_class = segment.__class__
|
||||||
return self.path_string
|
func, attr_names = segment_class_to_func_map[segment_class]
|
||||||
|
points = [
|
||||||
|
_convert_point_to_3d(*segment.__getattribute__(attr_name))
|
||||||
class InvalidPathError(ValueError):
|
for attr_name in attr_names
|
||||||
pass
|
]
|
||||||
|
func(*points)
|
||||||
|
|
||||||
class _PathStringParser:
|
|
||||||
# modified from https://github.com/regebro/svg.path/
|
|
||||||
def __init__(self, arguments, rules):
|
|
||||||
self.args = []
|
|
||||||
arguments = bytearray(arguments, "ascii")
|
|
||||||
self._strip_array(arguments)
|
|
||||||
while arguments:
|
|
||||||
for rule in rules:
|
|
||||||
self._rule_to_function_map[rule](arguments)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _rule_to_function_map(self):
|
|
||||||
return {
|
|
||||||
"x": self._get_number,
|
|
||||||
"y": self._get_number,
|
|
||||||
"a": self._get_number,
|
|
||||||
"u": self._get_unsigned_number,
|
|
||||||
"f": self._get_flag,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _strip_array(self, arg_array):
|
|
||||||
# wsp: (0x9, 0x20, 0xA, 0xC, 0xD) with comma 0x2C
|
|
||||||
# https://www.w3.org/TR/SVG/paths.html#PathDataBNF
|
|
||||||
while arg_array and arg_array[0] in [0x9, 0x20, 0xA, 0xC, 0xD, 0x2C]:
|
|
||||||
arg_array[0:1] = b""
|
|
||||||
|
|
||||||
def _get_number(self, arg_array):
|
|
||||||
pattern = re.compile(rb"^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?")
|
|
||||||
res = pattern.search(arg_array)
|
|
||||||
if not res:
|
|
||||||
raise InvalidPathError(f"Expected a number, got '{arg_array}'")
|
|
||||||
number = float(res.group())
|
|
||||||
self.args.append(number)
|
|
||||||
arg_array[res.start():res.end()] = b""
|
|
||||||
self._strip_array(arg_array)
|
|
||||||
return number
|
|
||||||
|
|
||||||
def _get_unsigned_number(self, arg_array):
|
|
||||||
number = self._get_number(arg_array)
|
|
||||||
if number < 0:
|
|
||||||
raise InvalidPathError(f"Expected an unsigned number, got '{number}'")
|
|
||||||
return number
|
|
||||||
|
|
||||||
def _get_flag(self, arg_array):
|
|
||||||
flag = arg_array[0]
|
|
||||||
if flag != 48 and flag != 49:
|
|
||||||
raise InvalidPathError(f"Expected a flag (0/1), got '{chr(flag)}'")
|
|
||||||
flag -= 48
|
|
||||||
self.args.append(flag)
|
|
||||||
arg_array[0:1] = b""
|
|
||||||
self._strip_array(arg_array)
|
|
||||||
return flag
|
|
|
@ -53,13 +53,19 @@ class SingleStringTex(VMobject):
|
||||||
sm.copy()
|
sm.copy()
|
||||||
for sm in tex_string_with_color_to_mob_map[(self.color, tex_string)]
|
for sm in tex_string_with_color_to_mob_map[(self.color, tex_string)]
|
||||||
))
|
))
|
||||||
self.init_colors(override=False)
|
self.init_colors()
|
||||||
|
|
||||||
if self.height is None:
|
if self.height is None:
|
||||||
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
|
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
|
||||||
if self.organize_left_to_right:
|
if self.organize_left_to_right:
|
||||||
self.organize_submobjects_left_to_right()
|
self.organize_submobjects_left_to_right()
|
||||||
|
|
||||||
|
def init_colors(self):
|
||||||
|
self.set_stroke(background=self.draw_stroke_behind_fill)
|
||||||
|
self.set_gloss(self.gloss)
|
||||||
|
self.set_flat_stroke(self.flat_stroke)
|
||||||
|
return self
|
||||||
|
|
||||||
def get_tex_file_body(self, tex_string):
|
def get_tex_file_body(self, tex_string):
|
||||||
new_tex = self.get_modified_expression(tex_string)
|
new_tex = self.get_modified_expression(tex_string)
|
||||||
if self.math_mode:
|
if self.math_mode:
|
||||||
|
|
|
@ -71,6 +71,8 @@ class Text(SVGMobject):
|
||||||
PangoUtils.remove_last_M(file_name)
|
PangoUtils.remove_last_M(file_name)
|
||||||
self.remove_empty_path(file_name)
|
self.remove_empty_path(file_name)
|
||||||
SVGMobject.__init__(self, file_name, **kwargs)
|
SVGMobject.__init__(self, file_name, **kwargs)
|
||||||
|
if self.color:
|
||||||
|
self.set_fill(self.color)
|
||||||
self.text = text
|
self.text = text
|
||||||
if self.disable_ligatures:
|
if self.disable_ligatures:
|
||||||
self.apply_space_chars()
|
self.apply_space_chars()
|
||||||
|
@ -85,9 +87,6 @@ class Text(SVGMobject):
|
||||||
if self.height is None:
|
if self.height is None:
|
||||||
self.scale(TEXT_MOB_SCALE_FACTOR)
|
self.scale(TEXT_MOB_SCALE_FACTOR)
|
||||||
|
|
||||||
def init_colors(self, override=True):
|
|
||||||
super().init_colors(override=override)
|
|
||||||
|
|
||||||
def remove_empty_path(self, file_name):
|
def remove_empty_path(self, file_name):
|
||||||
with open(file_name, 'r') as fpr:
|
with open(file_name, 'r') as fpr:
|
||||||
content = fpr.read()
|
content = fpr.read()
|
||||||
|
|
|
@ -260,7 +260,7 @@ class TexturedSurface(Surface):
|
||||||
super().init_uniforms()
|
super().init_uniforms()
|
||||||
self.uniforms["num_textures"] = self.num_textures
|
self.uniforms["num_textures"] = self.num_textures
|
||||||
|
|
||||||
def init_colors(self, override=True):
|
def init_colors(self):
|
||||||
self.data["opacity"] = np.array([self.uv_surface.data["rgbas"][:, 3]])
|
self.data["opacity"] = np.array([self.uv_surface.data["rgbas"][:, 3]])
|
||||||
|
|
||||||
def set_opacity(self, opacity, recurse=True):
|
def set_opacity(self, opacity, recurse=True):
|
||||||
|
|
|
@ -90,7 +90,7 @@ class VMobject(Mobject):
|
||||||
})
|
})
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
def init_colors(self, override=True):
|
def init_colors(self):
|
||||||
self.set_fill(
|
self.set_fill(
|
||||||
color=self.fill_color or self.color,
|
color=self.fill_color or self.color,
|
||||||
opacity=self.fill_opacity,
|
opacity=self.fill_opacity,
|
||||||
|
@ -103,9 +103,6 @@ class VMobject(Mobject):
|
||||||
)
|
)
|
||||||
self.set_gloss(self.gloss)
|
self.set_gloss(self.gloss)
|
||||||
self.set_flat_stroke(self.flat_stroke)
|
self.set_flat_stroke(self.flat_stroke)
|
||||||
if not override:
|
|
||||||
for submobjects in self.submobjects:
|
|
||||||
submobjects.init_colors(override=False)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_rgba_array(self, rgba_array, name=None, recurse=False):
|
def set_rgba_array(self, rgba_array, name=None, recurse=False):
|
||||||
|
|
|
@ -277,7 +277,6 @@ class DiscreteGraphScene(Scene):
|
||||||
def trace_cycle(self, cycle=None, color="yellow", run_time=2.0):
|
def trace_cycle(self, cycle=None, color="yellow", run_time=2.0):
|
||||||
if cycle is None:
|
if cycle is None:
|
||||||
cycle = self.graph.region_cycles[0]
|
cycle = self.graph.region_cycles[0]
|
||||||
time_per_edge = run_time / len(cycle)
|
|
||||||
next_in_cycle = it.cycle(cycle)
|
next_in_cycle = it.cycle(cycle)
|
||||||
next(next_in_cycle) # jump one ahead
|
next(next_in_cycle) # jump one ahead
|
||||||
self.traced_cycle = Mobject(*[
|
self.traced_cycle = Mobject(*[
|
||||||
|
|
|
@ -287,9 +287,6 @@ class LinearTransformationScene(VectorScene):
|
||||||
},
|
},
|
||||||
"background_plane_kwargs": {
|
"background_plane_kwargs": {
|
||||||
"color": GREY,
|
"color": GREY,
|
||||||
"axis_config": {
|
|
||||||
"stroke_color": GREY_B,
|
|
||||||
},
|
|
||||||
"axis_config": {
|
"axis_config": {
|
||||||
"color": GREY,
|
"color": GREY,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
argparse
|
|
||||||
colour
|
colour
|
||||||
numpy
|
numpy
|
||||||
Pillow
|
Pillow
|
||||||
|
@ -15,9 +14,9 @@ pygments
|
||||||
pyyaml
|
pyyaml
|
||||||
rich
|
rich
|
||||||
screeninfo
|
screeninfo
|
||||||
pyreadline; sys_platform == 'win32'
|
|
||||||
validators
|
validators
|
||||||
ipython
|
ipython
|
||||||
PyOpenGL
|
PyOpenGL
|
||||||
manimpango>=0.2.0,<0.4.0
|
manimpango>=0.2.0,<0.4.0
|
||||||
cssselect2
|
isosurfaces
|
||||||
|
svgelements
|
||||||
|
|
22
setup.cfg
22
setup.cfg
|
@ -1,6 +1,6 @@
|
||||||
[metadata]
|
[metadata]
|
||||||
name = manimgl
|
name = manimgl
|
||||||
version = 1.3.0
|
version = 1.4.1
|
||||||
author = Grant Sanderson
|
author = Grant Sanderson
|
||||||
author_email= grant@3blue1brown.com
|
author_email= grant@3blue1brown.com
|
||||||
description = Animation engine for explanatory math videos
|
description = Animation engine for explanatory math videos
|
||||||
|
@ -12,12 +12,24 @@ project_urls =
|
||||||
Documentation = https://3b1b.github.io/manim/
|
Documentation = https://3b1b.github.io/manim/
|
||||||
Source Code = https://github.com/3b1b/manim
|
Source Code = https://github.com/3b1b/manim
|
||||||
license = MIT
|
license = MIT
|
||||||
|
classifiers =
|
||||||
|
Development Status :: 4 - Beta
|
||||||
|
License :: OSI Approved :: MIT License
|
||||||
|
Topic :: Scientific/Engineering
|
||||||
|
Topic :: Multimedia :: Video
|
||||||
|
Topic :: Multimedia :: Graphics
|
||||||
|
Programming Language :: Python :: 3.6
|
||||||
|
Programming Language :: Python :: 3.7
|
||||||
|
Programming Language :: Python :: 3.8
|
||||||
|
Programming Language :: Python :: 3.9
|
||||||
|
Programming Language :: Python :: 3.10
|
||||||
|
Programming Language :: Python :: 3 :: Only
|
||||||
|
Natural Language :: English
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
packages = find:
|
packages = find:
|
||||||
include_package_data=True
|
include_package_data = True
|
||||||
install_requires =
|
install_requires =
|
||||||
argparse
|
|
||||||
colour
|
colour
|
||||||
numpy
|
numpy
|
||||||
Pillow
|
Pillow
|
||||||
|
@ -34,12 +46,12 @@ install_requires =
|
||||||
pyyaml
|
pyyaml
|
||||||
rich
|
rich
|
||||||
screeninfo
|
screeninfo
|
||||||
pyreadline; sys_platform == 'win32'
|
|
||||||
validators
|
validators
|
||||||
ipython
|
ipython
|
||||||
PyOpenGL
|
PyOpenGL
|
||||||
manimpango>=0.2.0,<0.4.0
|
manimpango>=0.2.0,<0.4.0
|
||||||
cssselect2
|
isosurfaces
|
||||||
|
svgelements
|
||||||
|
|
||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
|
|
Loading…
Add table
Reference in a new issue