yerba 0.0.3a0.dev0__tar.gz → 0.0.3a0.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {yerba-0.0.3a0.dev0/yerba.egg-info → yerba-0.0.3a0.dev2}/PKG-INFO +1 -1
- yerba-0.0.3a0.dev2/pyproject.toml +32 -0
- yerba-0.0.3a0.dev2/yerba/__init__.py +0 -0
- yerba-0.0.3a0.dev2/yerba/__main__.py +30 -0
- yerba-0.0.3a0.dev2/yerba/base/__init__.py +0 -0
- yerba-0.0.3a0.dev2/yerba/base/box.py +385 -0
- yerba-0.0.3a0.dev2/yerba/base/image.py +186 -0
- yerba-0.0.3a0.dev2/yerba/base/parser.py +167 -0
- yerba-0.0.3a0.dev2/yerba/base/properties.py +49 -0
- yerba-0.0.3a0.dev2/yerba/base/slide.py +194 -0
- yerba-0.0.3a0.dev2/yerba/base/template.py +887 -0
- yerba-0.0.3a0.dev2/yerba/base/template_protocol.py +147 -0
- yerba-0.0.3a0.dev2/yerba/base/ytext.py +206 -0
- yerba-0.0.3a0.dev2/yerba/defaults.py +73 -0
- yerba-0.0.3a0.dev2/yerba/logger_setup.py +28 -0
- yerba-0.0.3a0.dev2/yerba/main_rutine.py +205 -0
- yerba-0.0.3a0.dev2/yerba/managers/__init__.py +0 -0
- yerba-0.0.3a0.dev2/yerba/managers/color_manager.py +70 -0
- yerba-0.0.3a0.dev2/yerba/managers/id_manager.py +82 -0
- yerba-0.0.3a0.dev2/yerba/managers/singleton.py +13 -0
- yerba-0.0.3a0.dev2/yerba/templates/__init__.py +0 -0
- yerba-0.0.3a0.dev2/yerba/templates/basic.py +5 -0
- yerba-0.0.3a0.dev2/yerba/templates/nice.py +83 -0
- yerba-0.0.3a0.dev2/yerba/utils/__init__.py +0 -0
- yerba-0.0.3a0.dev2/yerba/utils/aux_classes.py +49 -0
- yerba-0.0.3a0.dev2/yerba/utils/aux_functions.py +159 -0
- yerba-0.0.3a0.dev2/yerba/utils/constants.py +36 -0
- yerba-0.0.3a0.dev2/yerba/utils/latex.py +154 -0
- {yerba-0.0.3a0.dev0 → yerba-0.0.3a0.dev2/yerba.egg-info}/PKG-INFO +1 -1
- yerba-0.0.3a0.dev2/yerba.egg-info/SOURCES.txt +35 -0
- yerba-0.0.3a0.dev2/yerba.egg-info/top_level.txt +1 -0
- yerba-0.0.3a0.dev0/pyproject.toml +0 -28
- yerba-0.0.3a0.dev0/yerba.egg-info/SOURCES.txt +0 -9
- yerba-0.0.3a0.dev0/yerba.egg-info/top_level.txt +0 -1
- {yerba-0.0.3a0.dev0 → yerba-0.0.3a0.dev2}/LICENSE +0 -0
- {yerba-0.0.3a0.dev0 → yerba-0.0.3a0.dev2}/README.md +0 -0
- {yerba-0.0.3a0.dev0 → yerba-0.0.3a0.dev2}/setup.cfg +0 -0
- {yerba-0.0.3a0.dev0 → yerba-0.0.3a0.dev2}/yerba.egg-info/dependency_links.txt +0 -0
- {yerba-0.0.3a0.dev0 → yerba-0.0.3a0.dev2}/yerba.egg-info/entry_points.txt +0 -0
- {yerba-0.0.3a0.dev0 → yerba-0.0.3a0.dev2}/yerba.egg-info/requires.txt +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "yerba"
|
|
7
|
+
version = "0.0.3-alpha-dev2"
|
|
8
|
+
requires-python = ">=3.8"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"manim",
|
|
11
|
+
"manim-mobject-svg",
|
|
12
|
+
"markdown-it-py",
|
|
13
|
+
"mdformat",
|
|
14
|
+
"mdit-py-plugins",
|
|
15
|
+
"numpy",
|
|
16
|
+
"pillow",
|
|
17
|
+
"pyyaml"
|
|
18
|
+
]
|
|
19
|
+
authors = [
|
|
20
|
+
{name = "Bernardo L. Español", email = "esp.bernardo@gmail.com"},
|
|
21
|
+
]
|
|
22
|
+
readme = "README.md"
|
|
23
|
+
license = {file = "LICENSE.txt"}
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
yerba = "yerba.__main__:cli_entry"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["."]
|
|
30
|
+
include = ["yerba*"]
|
|
31
|
+
exclude = []
|
|
32
|
+
namespaces = false
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from .logger_setup import logger
|
|
5
|
+
from .main_rutine import MainRutine
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def cli_entry():
|
|
9
|
+
if len(sys.argv) == 1:
|
|
10
|
+
logger.info("You should specify an input filename")
|
|
11
|
+
quit()
|
|
12
|
+
elif len(sys.argv) == 2:
|
|
13
|
+
filename = sys.argv[1]
|
|
14
|
+
if os.path.exists(filename):
|
|
15
|
+
pass
|
|
16
|
+
elif os.path.exists(filename+".md"):
|
|
17
|
+
filename = filename+".md"
|
|
18
|
+
else:
|
|
19
|
+
logger.error(f"File '{filename}' not found")
|
|
20
|
+
quit()
|
|
21
|
+
else:
|
|
22
|
+
logger.error("Too many args")
|
|
23
|
+
quit()
|
|
24
|
+
|
|
25
|
+
main_rutine = MainRutine(filename)
|
|
26
|
+
main_rutine.run()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
cli_entry()
|
|
File without changes
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import numpy as np
|
|
3
|
+
from manim import VMobject, VGroup, Rectangle
|
|
4
|
+
|
|
5
|
+
from ..defaults import box_params
|
|
6
|
+
from ..managers.color_manager import ColorManager
|
|
7
|
+
from ..utils.constants import (
|
|
8
|
+
UP, DOWN, LEFT, RIGHT, ORIGIN, SLIDE_HEIGHT, SLIDE_WIDTH, LEFT_EDGE,
|
|
9
|
+
RIGHT_EDGE, SLIDE_X_RAD, SLIDE_Y_RAD, TOP_EDGE, BOTTOM_EDGE, UL, DR
|
|
10
|
+
)
|
|
11
|
+
from ..utils.aux_functions import (
|
|
12
|
+
restructure_list_to_exclude_certain_family_members,
|
|
13
|
+
replace_in_list
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Box():
|
|
18
|
+
def __init__(self, center: np.ndarray,
|
|
19
|
+
width: float, height: float, arrange: str | None = "top left",
|
|
20
|
+
arrange_buff: float = box_params["arrange_buff"],
|
|
21
|
+
is_null: bool = False, is_unique=False):
|
|
22
|
+
|
|
23
|
+
self.center = center
|
|
24
|
+
self.width = width
|
|
25
|
+
self.height = height
|
|
26
|
+
self.arrange = None if arrange == "none" else arrange
|
|
27
|
+
self.arrange_buff = arrange_buff
|
|
28
|
+
self.is_null = is_null
|
|
29
|
+
self.is_unique = is_unique
|
|
30
|
+
|
|
31
|
+
self.grid: dict[str, Box] | None = None
|
|
32
|
+
self.mobjects: list = []
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_vertex(cls, ul_vertex: tuple[float, float],
|
|
36
|
+
dr_vertex: tuple[float, float], **kwargs):
|
|
37
|
+
left_edge, top_edge = ul_vertex
|
|
38
|
+
right_edge, bottom_edge = dr_vertex
|
|
39
|
+
center = np.array(
|
|
40
|
+
[(right_edge+left_edge)/2, (top_edge+bottom_edge)/2, 0])
|
|
41
|
+
width = right_edge-left_edge
|
|
42
|
+
height = top_edge-bottom_edge
|
|
43
|
+
return cls(center, width, height, **kwargs)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_mo(cls, mo, **kwargs):
|
|
47
|
+
return cls.from_vertex(
|
|
48
|
+
ul_vertex=mo.get_corner(UL)[:-1],
|
|
49
|
+
dr_vertex=mo.get_corner(DR)[:-1],
|
|
50
|
+
**kwargs
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_box(cls, box):
|
|
55
|
+
return cls(center=box.center.copy(),
|
|
56
|
+
width=box.width, height=box.height,
|
|
57
|
+
arrange=box.arrange, is_unique=True)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def get_full_box(cls, **kwargs):
|
|
61
|
+
return cls(ORIGIN, SLIDE_WIDTH, SLIDE_HEIGHT, **kwargs)
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def get_left_box(cls, width, **kwargs):
|
|
65
|
+
return cls(LEFT_EDGE+RIGHT*width/2, width, SLIDE_HEIGHT, **kwargs)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def get_right_box(cls, width, **kwargs):
|
|
69
|
+
return cls(RIGHT_EDGE+LEFT*width/2, width, SLIDE_HEIGHT, **kwargs)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def get_top_box(cls, height, **kwargs):
|
|
73
|
+
return cls(TOP_EDGE+DOWN*height/2, SLIDE_WIDTH, height, **kwargs)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def get_bottom_box(cls, height, **kwargs):
|
|
77
|
+
return cls(BOTTOM_EDGE+UP*height/2, SLIDE_WIDTH, height, **kwargs)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def get_inner_box(cls, *, left_box=None, right_box=None,
|
|
81
|
+
top_box=None, bottom_box=None, arrange):
|
|
82
|
+
|
|
83
|
+
lg = SLIDE_X_RAD + left_box.center[0] + left_box.width/2 \
|
|
84
|
+
if left_box else 0
|
|
85
|
+
rg = SLIDE_X_RAD - right_box.center[0] + right_box.width/2 \
|
|
86
|
+
if right_box else 0
|
|
87
|
+
tg = SLIDE_Y_RAD - top_box.center[1] + top_box.height/2 \
|
|
88
|
+
if top_box else 0
|
|
89
|
+
bg = SLIDE_Y_RAD + bottom_box.center[1] + bottom_box.height/2 \
|
|
90
|
+
if bottom_box else 0
|
|
91
|
+
|
|
92
|
+
box = cls.get_full_box(arrange=arrange)
|
|
93
|
+
box.shrink(left_gap=lg, right_gap=rg, top_gap=tg, bottom_gap=bg)
|
|
94
|
+
|
|
95
|
+
return box
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def get_null_box(cls):
|
|
99
|
+
return cls(ORIGIN, SLIDE_WIDTH, SLIDE_HEIGHT, is_null=True)
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def merge_boxes(boxes, arrange):
|
|
103
|
+
if len(boxes) == 1:
|
|
104
|
+
return boxes[0]
|
|
105
|
+
left_edge = min([b.center[0]-b.width/2 for b in boxes])
|
|
106
|
+
right_edge = max([b.center[0]+b.width/2 for b in boxes])
|
|
107
|
+
top_edge = max([b.center[1]+b.height/2 for b in boxes])
|
|
108
|
+
bottom_edge = min([b.center[1]-b.height/2 for b in boxes])
|
|
109
|
+
|
|
110
|
+
return Box.from_vertex(ul_vertex=(left_edge, top_edge),
|
|
111
|
+
dr_vertex=(right_edge, bottom_edge),
|
|
112
|
+
arrange=arrange)
|
|
113
|
+
|
|
114
|
+
def add(self, mobject) -> None:
|
|
115
|
+
if hasattr(mobject, "width_units") and mobject.width_units == "box":
|
|
116
|
+
mobject.set(width=self.width*mobject.width, width_units="manim")
|
|
117
|
+
if hasattr(mobject, "height_units") and mobject.height_units == "box":
|
|
118
|
+
mobject.set(height=self.height*mobject.height,
|
|
119
|
+
height_units="manim")
|
|
120
|
+
|
|
121
|
+
self.mobjects.append(mobject)
|
|
122
|
+
|
|
123
|
+
def remove(self, mobjects):
|
|
124
|
+
"""Remove specified mobjects from the box."""
|
|
125
|
+
new_l = restructure_list_to_exclude_certain_family_members(
|
|
126
|
+
self.mobjects, mobjects)
|
|
127
|
+
self.mobjects = new_l
|
|
128
|
+
|
|
129
|
+
def replace(self, old_mo, new_mo):
|
|
130
|
+
"""Remove specified mobjects from the box."""
|
|
131
|
+
replace_in_list(self.mobjects, old_mo, new_mo)
|
|
132
|
+
|
|
133
|
+
def remove_all_mobjects(self) -> None:
|
|
134
|
+
self.mobjects = []
|
|
135
|
+
|
|
136
|
+
def auto_arrange(self) -> None:
|
|
137
|
+
"""TODO(bersp): DOC"""
|
|
138
|
+
if self.arrange is None or len(self.mobjects) == 0 or self.is_null:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
centers = []
|
|
142
|
+
for mo in self.mobjects:
|
|
143
|
+
centers.append(mo.get_center())
|
|
144
|
+
mo.move_to(ORIGIN)
|
|
145
|
+
|
|
146
|
+
if self.arrange == "center":
|
|
147
|
+
(VGroup(*self.mobjects)
|
|
148
|
+
.move_to(self.center))
|
|
149
|
+
elif self.arrange == "hcenter":
|
|
150
|
+
(VGroup(*self.mobjects)
|
|
151
|
+
.arrange(direction=DOWN, buff=self.arrange_buff)
|
|
152
|
+
.move_to(self.center))
|
|
153
|
+
elif self.arrange == "top center":
|
|
154
|
+
(VGroup(*self.mobjects)
|
|
155
|
+
.arrange(DOWN, buff=self.arrange_buff)
|
|
156
|
+
.next_to(self.get_top(), DOWN, buff=0))
|
|
157
|
+
elif self.arrange == "top left":
|
|
158
|
+
(VGroup(*self.mobjects)
|
|
159
|
+
.arrange(DOWN, buff=self.arrange_buff, aligned_edge=LEFT)
|
|
160
|
+
.next_to(self.get_corner(LEFT, UP), DOWN+RIGHT, buff=0))
|
|
161
|
+
elif self.arrange == "top right":
|
|
162
|
+
(VGroup(*self.mobjects)
|
|
163
|
+
.arrange(DOWN, buff=self.arrange_buff, aligned_edge=RIGHT)
|
|
164
|
+
.next_to(self.get_corner(RIGHT, UP), DOWN+LEFT, buff=0))
|
|
165
|
+
elif self.arrange == "center left":
|
|
166
|
+
(VGroup(*self.mobjects)
|
|
167
|
+
.arrange(DOWN, buff=self.arrange_buff, aligned_edge=LEFT)
|
|
168
|
+
.next_to(self.get_left(), RIGHT, buff=0))
|
|
169
|
+
elif self.arrange == "center right":
|
|
170
|
+
(VGroup(*self.mobjects)
|
|
171
|
+
.arrange(DOWN, buff=self.arrange_buff, aligned_edge=RIGHT)
|
|
172
|
+
.next_to(self.get_right(), LEFT, buff=0))
|
|
173
|
+
else:
|
|
174
|
+
raise ValueError(f"Arrange {self.arrange!r} is not defined")
|
|
175
|
+
|
|
176
|
+
for cent, mo in zip(centers, self.mobjects):
|
|
177
|
+
|
|
178
|
+
if hasattr(mo, 'box_arrange'):
|
|
179
|
+
if mo.box_arrange == "none":
|
|
180
|
+
pass
|
|
181
|
+
elif mo.box_arrange == "center":
|
|
182
|
+
mo.move_to(self.center)
|
|
183
|
+
elif mo.box_arrange == "hcenter":
|
|
184
|
+
mo.set_x(self.center[0])
|
|
185
|
+
elif mo.box_arrange == "vcenter":
|
|
186
|
+
mo.set_y(self.center[1])
|
|
187
|
+
else:
|
|
188
|
+
ValueError(
|
|
189
|
+
f"'box arrange' {mo.box_arrange!r} is not defined")
|
|
190
|
+
|
|
191
|
+
mo.shift(cent)
|
|
192
|
+
|
|
193
|
+
def def_grid(self, grid, hspace=0.5, vspace=0.5,
|
|
194
|
+
width_ratios=None, height_ratios=None,
|
|
195
|
+
arrange="self") -> dict[str, Box]:
|
|
196
|
+
"""
|
|
197
|
+
TODO(bersp): Handle errors (bad defined grid)
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
if arrange == "self":
|
|
201
|
+
arrange = self.arrange
|
|
202
|
+
|
|
203
|
+
grid = np.asarray(grid)
|
|
204
|
+
nrows, ncols = grid.shape
|
|
205
|
+
|
|
206
|
+
if width_ratios is None:
|
|
207
|
+
width_ratios = np.asarray([1]*ncols)/ncols
|
|
208
|
+
else:
|
|
209
|
+
width_ratios = np.asarray(width_ratios)
|
|
210
|
+
width_ratios = width_ratios/width_ratios.sum()
|
|
211
|
+
|
|
212
|
+
if height_ratios is None:
|
|
213
|
+
height_ratios = np.asarray([1]*nrows)/nrows
|
|
214
|
+
else:
|
|
215
|
+
height_ratios = np.asarray(height_ratios)
|
|
216
|
+
height_ratios = height_ratios/height_ratios.sum()
|
|
217
|
+
|
|
218
|
+
grid_widths = width_ratios * (self.width-hspace*(ncols-1))
|
|
219
|
+
grid_heights = height_ratios * (self.height-vspace*(nrows-1))
|
|
220
|
+
|
|
221
|
+
centers_x = []
|
|
222
|
+
cx = -self.width/2 - hspace
|
|
223
|
+
for gw in grid_widths:
|
|
224
|
+
cx += gw/2 + hspace
|
|
225
|
+
centers_x.append(cx)
|
|
226
|
+
cx += gw/2
|
|
227
|
+
|
|
228
|
+
centers_y = []
|
|
229
|
+
cy = self.height/2 + hspace
|
|
230
|
+
for gh in grid_heights:
|
|
231
|
+
cy -= gh/2 + hspace
|
|
232
|
+
centers_y.append(cy)
|
|
233
|
+
cy -= gh/2
|
|
234
|
+
|
|
235
|
+
x0, y0, _ = self.center
|
|
236
|
+
subgrid = np.zeros(shape=(nrows, ncols), dtype='object')
|
|
237
|
+
for row, (cy, gh) in enumerate(zip(centers_y, grid_heights)):
|
|
238
|
+
for col, (cx, gw) in enumerate(zip(centers_x, grid_widths)):
|
|
239
|
+
subgrid[row, col] = Box(center=np.array((x0+cx, y0+cy, 0)),
|
|
240
|
+
height=gh, width=gw, arrange=arrange)
|
|
241
|
+
|
|
242
|
+
self.grid = {}
|
|
243
|
+
for label in np.unique(grid):
|
|
244
|
+
self.grid[label] = Box.merge_boxes(
|
|
245
|
+
subgrid[np.where(grid == label)], arrange=arrange
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return self.grid
|
|
249
|
+
|
|
250
|
+
def get_bbox_mo(self, color="BLACK") -> VMobject:
|
|
251
|
+
color = ColorManager().get_color(color)
|
|
252
|
+
r = (
|
|
253
|
+
Rectangle(height=self.height, width=self.width,
|
|
254
|
+
fill_color=color, fill_opacity=0.2)
|
|
255
|
+
.set_stroke(color=color, opacity=0.0)
|
|
256
|
+
.move_to(self.center)
|
|
257
|
+
.set_z_index(-1)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return r
|
|
261
|
+
|
|
262
|
+
def get_bbox_grid(self, color="RED") -> VMobject:
|
|
263
|
+
color = ColorManager().get_color(color)
|
|
264
|
+
|
|
265
|
+
if self.grid is None:
|
|
266
|
+
raise ValueError(f"{self!r} has no defined grid")
|
|
267
|
+
|
|
268
|
+
o = VGroup()
|
|
269
|
+
for g in self.grid.values():
|
|
270
|
+
o.add(g.get_bbox_mo(color=color))
|
|
271
|
+
|
|
272
|
+
return o
|
|
273
|
+
|
|
274
|
+
def shrink(self, *, left_gap=0, right_gap=0,
|
|
275
|
+
top_gap=0, bottom_gap=0) -> Box:
|
|
276
|
+
self.width = self.width - (left_gap + right_gap)
|
|
277
|
+
self.height = self.height - (top_gap + bottom_gap)
|
|
278
|
+
self.center = np.array([self.center[0] + (left_gap - right_gap)/2,
|
|
279
|
+
self.center[1] + (bottom_gap - top_gap)/2,
|
|
280
|
+
0])
|
|
281
|
+
return self
|
|
282
|
+
|
|
283
|
+
# ---
|
|
284
|
+
|
|
285
|
+
def set_center(self, value):
|
|
286
|
+
self.center = value
|
|
287
|
+
return self
|
|
288
|
+
|
|
289
|
+
def set_width(self, value):
|
|
290
|
+
self.width = value
|
|
291
|
+
return self
|
|
292
|
+
|
|
293
|
+
def set_height(self, value):
|
|
294
|
+
self.height = value
|
|
295
|
+
return self
|
|
296
|
+
|
|
297
|
+
def set_arrange(self, value: str | None):
|
|
298
|
+
self.arrange = None if value == "none" else value
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
def set_arrange_buff(self, value):
|
|
302
|
+
self.arrange_buff = value
|
|
303
|
+
return self
|
|
304
|
+
|
|
305
|
+
def null(self, value=True):
|
|
306
|
+
self.is_null = value
|
|
307
|
+
return self
|
|
308
|
+
|
|
309
|
+
# ---
|
|
310
|
+
|
|
311
|
+
def get_left(self):
|
|
312
|
+
return self.center + self.width/2*LEFT
|
|
313
|
+
|
|
314
|
+
def get_right(self):
|
|
315
|
+
return self.center + self.width/2*RIGHT
|
|
316
|
+
|
|
317
|
+
def get_top(self):
|
|
318
|
+
return self.center + self.height/2*UP
|
|
319
|
+
|
|
320
|
+
def get_bottom(self):
|
|
321
|
+
return self.center + self.height/2*DOWN
|
|
322
|
+
|
|
323
|
+
def get_corner(self, x_direction, y_direction):
|
|
324
|
+
return self.center+self.width/2*x_direction+self.height/2*y_direction
|
|
325
|
+
|
|
326
|
+
# ---
|
|
327
|
+
|
|
328
|
+
def __eq__(self, other_box):
|
|
329
|
+
if self.is_unique or other_box.is_unique:
|
|
330
|
+
return False
|
|
331
|
+
return (np.all(self.center == other_box.center) and
|
|
332
|
+
self.width == other_box.width and
|
|
333
|
+
self.height == other_box.height and
|
|
334
|
+
self.arrange == other_box.arrange and
|
|
335
|
+
self.arrange_buff == other_box.arrange_buff and
|
|
336
|
+
self.is_null == other_box.is_null)
|
|
337
|
+
|
|
338
|
+
def __repr__(self):
|
|
339
|
+
c = f"[{self.center[0]:.2g}, {self.center[1]:.2g}]"
|
|
340
|
+
return f"Box(center={c}, width={self.width:.2g}, height={self.height:.2g}, arrange={self.arrange}, is_null={self.is_null})"
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class NamedBoxes:
|
|
344
|
+
def __init__(self, **boxes):
|
|
345
|
+
"""
|
|
346
|
+
Parameters
|
|
347
|
+
----------
|
|
348
|
+
**boxes : dict
|
|
349
|
+
Named boxes
|
|
350
|
+
"""
|
|
351
|
+
for name, box in boxes.items():
|
|
352
|
+
self.add(name, box)
|
|
353
|
+
|
|
354
|
+
def add(self, name, box):
|
|
355
|
+
setattr(self, name, box)
|
|
356
|
+
|
|
357
|
+
def set_current_box(self, box_or_box_name):
|
|
358
|
+
"""
|
|
359
|
+
TODO(bersp): Documentation.
|
|
360
|
+
TODO(bersp): Handle errors.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
if isinstance(box_or_box_name, Box):
|
|
364
|
+
setattr(self, "active", box_or_box_name)
|
|
365
|
+
else:
|
|
366
|
+
setattr(self, "active", self.__dict__[box_or_box_name])
|
|
367
|
+
|
|
368
|
+
def remove_all_mobjects(self):
|
|
369
|
+
"""Remove all mobjects from all boxes in the slide layout."""
|
|
370
|
+
for _, box in self.__dict__.items():
|
|
371
|
+
box.remove_all_mobjects()
|
|
372
|
+
|
|
373
|
+
def get_bbox_mo(self):
|
|
374
|
+
"""Return a VGroup containing all the bounding boxes of the all boxes."""
|
|
375
|
+
o = VGroup()
|
|
376
|
+
for _, box in self.__dict__.items():
|
|
377
|
+
o += box.get_bbox_mo()
|
|
378
|
+
return o
|
|
379
|
+
|
|
380
|
+
def __repr__(self):
|
|
381
|
+
s = ''
|
|
382
|
+
for name, box in self.__dict__.items():
|
|
383
|
+
s += f"{name} -> {box}\n"
|
|
384
|
+
|
|
385
|
+
return s
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import subprocess
|
|
4
|
+
import shutil
|
|
5
|
+
import uuid
|
|
6
|
+
from manim import VGroup, Rectangle
|
|
7
|
+
from xml.etree import ElementTree
|
|
8
|
+
from PIL import Image
|
|
9
|
+
|
|
10
|
+
from ..managers.color_manager import ColorManager
|
|
11
|
+
from ..base.ytext import Ytex
|
|
12
|
+
from ..utils.constants import UL, PX, SLIDE_X_RAD, SLIDE_Y_RAD
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ImageSvgBase(VGroup):
|
|
16
|
+
# TODO(bersp): Implement .rotate to imgs
|
|
17
|
+
def __init__(self, filename, width, height, draft_mode):
|
|
18
|
+
|
|
19
|
+
self.filename = filename
|
|
20
|
+
self.basename = os.path.basename(filename)
|
|
21
|
+
self.draft_mode = draft_mode
|
|
22
|
+
|
|
23
|
+
colors = ColorManager()
|
|
24
|
+
|
|
25
|
+
rec = (Rectangle(width=width, height=height)
|
|
26
|
+
.set_stroke(opacity=0)
|
|
27
|
+
.set_fill(color=colors.get_color("BLACK"), opacity=0.8))
|
|
28
|
+
|
|
29
|
+
if draft_mode:
|
|
30
|
+
tex_filename = filename.replace("_", r"\\_")
|
|
31
|
+
tex_filename = fr"\\texttt{{{tex_filename}}}"
|
|
32
|
+
super().__init__(
|
|
33
|
+
rec,
|
|
34
|
+
Ytex(tex_filename, color=colors.get_color("BLACK")).move_to(
|
|
35
|
+
rec).set(width=width*0.9)
|
|
36
|
+
)
|
|
37
|
+
else:
|
|
38
|
+
super().__init__(rec)
|
|
39
|
+
|
|
40
|
+
def _manim_to_svg_coords(self):
|
|
41
|
+
x0, y0 = self.get_corner(UL)[:2]
|
|
42
|
+
x0 = (SLIDE_X_RAD+x0)*PX
|
|
43
|
+
y0 = (SLIDE_Y_RAD-y0)*PX
|
|
44
|
+
w = self.width*PX
|
|
45
|
+
h = self.height*PX
|
|
46
|
+
|
|
47
|
+
return x0, y0, w, h
|
|
48
|
+
|
|
49
|
+
def _get_width_and_height(self, width, height,
|
|
50
|
+
file_width, file_height):
|
|
51
|
+
|
|
52
|
+
ERR = ValueError(
|
|
53
|
+
r"width or height must be numbers or percentages like '90%'"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
porcentage_pattern = r"(^\d+(\.\d+)?)%$"
|
|
57
|
+
|
|
58
|
+
if isinstance(width, str):
|
|
59
|
+
m = re.match(porcentage_pattern, width)
|
|
60
|
+
if m:
|
|
61
|
+
self.width_units = "box"
|
|
62
|
+
width = float(m[1])/100
|
|
63
|
+
else:
|
|
64
|
+
raise ERR
|
|
65
|
+
|
|
66
|
+
if isinstance(height, str):
|
|
67
|
+
m = re.match(porcentage_pattern, height)
|
|
68
|
+
if m:
|
|
69
|
+
self.height_units = "box"
|
|
70
|
+
height = float(m[1])/100
|
|
71
|
+
else:
|
|
72
|
+
raise ERR
|
|
73
|
+
|
|
74
|
+
if not ((isinstance(width, (int, float)) or width is None) and
|
|
75
|
+
(isinstance(height, (int, float)) or height is None)):
|
|
76
|
+
raise ERR
|
|
77
|
+
|
|
78
|
+
if width is not None and height is None:
|
|
79
|
+
height = file_height/file_width*width
|
|
80
|
+
elif width is None and height is not None:
|
|
81
|
+
width = file_width/file_height*height
|
|
82
|
+
else:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"You have to specify either 'width' or 'height', and not both")
|
|
85
|
+
|
|
86
|
+
return width, height
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ImageSvg(ImageSvgBase):
|
|
90
|
+
def __init__(self, filename, width=None, height=None,
|
|
91
|
+
draft_mode=False, **_):
|
|
92
|
+
|
|
93
|
+
fw, fh = Image.open(filename).size
|
|
94
|
+
width, height = self._get_width_and_height(width, height, fw, fh)
|
|
95
|
+
|
|
96
|
+
super().__init__(filename, width, height, draft_mode)
|
|
97
|
+
|
|
98
|
+
def get_svg_str(self):
|
|
99
|
+
x0, y0, w, h = self._manim_to_svg_coords()
|
|
100
|
+
img_base64 = (
|
|
101
|
+
subprocess.run(["base64", self.filename],
|
|
102
|
+
stdout=subprocess.PIPE)
|
|
103
|
+
.stdout.decode("utf-8").replace("\n", "")
|
|
104
|
+
)
|
|
105
|
+
s = (f'<image width="{w}" height="{h}" x="{x0}" y="{y0}" '
|
|
106
|
+
f'href="data:image/png;base64,{img_base64}"/>')
|
|
107
|
+
|
|
108
|
+
return s
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ImagePDFSvg(ImageSvgBase):
|
|
112
|
+
def __init__(self, filename, width=None, height=None,
|
|
113
|
+
backend='poppler', draft_mode=False, **_):
|
|
114
|
+
if not shutil.which("inkscape"):
|
|
115
|
+
raise FileNotFoundError("Inkscape is required to use PDF images.")
|
|
116
|
+
|
|
117
|
+
svg_str_raw = self._get_svg_str_raw(filename, backend)
|
|
118
|
+
self.xml_tree = ElementTree.fromstring(svg_str_raw)
|
|
119
|
+
|
|
120
|
+
unique_prefix = f"{uuid.uuid4().hex[:8]}_"
|
|
121
|
+
self._rename_ids(self.xml_tree, unique_prefix)
|
|
122
|
+
|
|
123
|
+
fw, fh = self.xml_tree.get('width'), self.xml_tree.get('height')
|
|
124
|
+
if fw is not None and fh is not None:
|
|
125
|
+
fw = float(fw.replace("pt", ""))
|
|
126
|
+
fh = float(fh.replace("pt", ""))
|
|
127
|
+
else:
|
|
128
|
+
raise ValueError("An error occurred while parsing the PDF "
|
|
129
|
+
f"file {filename!r} to SVG.")
|
|
130
|
+
|
|
131
|
+
width, height = self._get_width_and_height(width, height, fw, fh)
|
|
132
|
+
|
|
133
|
+
super().__init__(filename, width, height, draft_mode)
|
|
134
|
+
|
|
135
|
+
def get_svg_str(self):
|
|
136
|
+
x0, y0, w, h = self._manim_to_svg_coords()
|
|
137
|
+
|
|
138
|
+
self.xml_tree.set("x", str(x0))
|
|
139
|
+
self.xml_tree.set("y", str(y0))
|
|
140
|
+
self.xml_tree.set("width", str(w))
|
|
141
|
+
self.xml_tree.set("height", str(h))
|
|
142
|
+
|
|
143
|
+
s = ElementTree.tostring(self.xml_tree, encoding='unicode')
|
|
144
|
+
return s
|
|
145
|
+
|
|
146
|
+
def _get_svg_str_raw(self, filename, backend):
|
|
147
|
+
assert backend == "poppler" or backend == "internal", (
|
|
148
|
+
"Backend must be 'poppler' or 'internal'"
|
|
149
|
+
)
|
|
150
|
+
opts = ["--export-plain-svg",
|
|
151
|
+
"--export-type=svg",
|
|
152
|
+
"--export-filename=-"]
|
|
153
|
+
if backend == "poppler":
|
|
154
|
+
opts.append("--pdf-poppler")
|
|
155
|
+
s = subprocess.run(["inkscape", *opts, filename],
|
|
156
|
+
stdout=subprocess.PIPE, stderr=open(os.devnull, "w")
|
|
157
|
+
).stdout.decode("utf-8")
|
|
158
|
+
|
|
159
|
+
return s
|
|
160
|
+
|
|
161
|
+
def _rename_ids(self, root, prefix):
|
|
162
|
+
ids_map = {}
|
|
163
|
+
for elem in root.iter():
|
|
164
|
+
if 'id' in elem.attrib:
|
|
165
|
+
old_id = elem.get('id')
|
|
166
|
+
new_id = prefix + old_id
|
|
167
|
+
elem.set('id', new_id)
|
|
168
|
+
ids_map[old_id] = new_id
|
|
169
|
+
|
|
170
|
+
if not ids_map:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
escaped_ids: list[str] = list(map(re.escape, ids_map.keys()))
|
|
174
|
+
|
|
175
|
+
pattern = re.compile(r"#(" + "|".join(escaped_ids) + r")\b")
|
|
176
|
+
|
|
177
|
+
def replace_id(m):
|
|
178
|
+
old = m.group(1)
|
|
179
|
+
return "#" + ids_map[old]
|
|
180
|
+
|
|
181
|
+
for elem in root.iter():
|
|
182
|
+
for attr_name in list(elem.attrib.keys()):
|
|
183
|
+
attr_value = elem.attrib[attr_name]
|
|
184
|
+
if "#" in attr_value: # Quick verification to not apply re if not necessary
|
|
185
|
+
new_attr_value = pattern.sub(replace_id, attr_value)
|
|
186
|
+
elem.set(attr_name, new_attr_value)
|