fix: fixed bad text slicing for Paragraph's lines (#2721)

* fix: fixed bad text slicing for Paragraph's lines

Inside Paragraph's _gen_chars method, a bad string slicing caused the text's characters to be obtained in an incorrect order. This would cause chars (and thus self.chars and self.lines in __init__) to store incorrect values.
If Paragraph's alignment parameter was None, nothing wrong seemed to happen, but if alignment was 'left', 'center or 'right', the text's lines would not display correctly.
This happened because the slicing was of the form [begin : begin + string_length + 1]. This bug was fixed by changing it to [begin : begin + char_count], where char_count is the number of characters in the corresponding string which are not " ", "\n" or "\t".
I've also cleaned __init__'s code:
- There was a redundant fragment of code which did exactly the same thing as _gen_chars, storing data in chars_lines_text_list. I deleted this code and used self.chars instead.
- lines_str joins the text strings with '\n', but then lines_str_list split lines_str again, with '\n' as delimiter. I rewrote lines_str_list as just list(text).
- self.lines is a list which essentially stores list(self.chars) as its first element. The original code used a for loop to append self.chars' lines, one by one, to self.lines[0]. I replaced this for loop with list(self.chars).
- self.lines' second element was rewritten as a repetition of [self.alignment], self.chars.__len()__ times.
- I replaced the for loop used to append the line centers to self.lines_initial_positions, and instead used [line.get_center() for line in self.lines[0]].

* revert: reverted lines_str and lines_str_list changes

* fix: added Paragraph.consider_spaces_as_chars to fix issue

* fix: changed `config` kwargs name to `kwargs`

Using `config` as a name for keyword arguments in `Paragraph.__init__` is inconsistent with the use of `kwargs` everywhere else and also shadows the `config` global variable, which is why this variable was renamed as `kwargs`.

* refactor: changed `var.__len__()` to `len(var)`

Changed 8 appearances (1 in `remove_invisible_chars`, 6 in `Paragraph` and 1 in `Text`) of magic method `__len__` (as in `var.__len__()`) to `len(var)`.

* refactor: changed docstrings and added type hints

* fix: changed space char hardcoding to isspace() string method

Co-authored-by: Tristan Schulz <mrdiverlp@gmail.com>
This commit is contained in:
Francisco Manríquez Novoa 2022-06-20 15:48:56 -04:00 committed by GitHub
commit b5681fd905
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -79,12 +79,12 @@ DEFAULT_LINE_SPACING_SCALE = 0.3
TEXT2SVG_ADJUSTMENT_FACTOR = 4.8
def remove_invisible_chars(mobject):
def remove_invisible_chars(mobject: SVGMobject) -> SVGMobject:
"""Function to remove unwanted invisible characters from some mobjects.
Parameters
----------
mobject : :class:`~.SVGMobject`
mobject
Any SVGMobject from which we want to remove unwanted invisible characters.
Returns
@ -102,7 +102,7 @@ def remove_invisible_chars(mobject):
mobject = mobject.code
mobject_without_dots = VGroup()
if mobject[0].__class__ == VGroup:
for i in range(mobject.__len__()):
for i in range(len(mobject)):
mobject_without_dots.add(VGroup())
mobject_without_dots[i].add(*(k for k in mobject[i] if k.__class__ != Dot))
else:
@ -123,10 +123,10 @@ class Paragraph(VGroup):
Parameters
----------
line_spacing : :class:`float`, optional
Represents the spacing between lines. Default to -1, which means auto.
alignment : :class:`str`, optional
Defines the alignment of paragraph. Default to "left". Possible values are "left", "right", "center"
line_spacing
Represents the spacing between lines. Defaults to -1, which means auto.
alignment
Defines the alignment of paragraph. Defaults to None. Possible values are "left", "right" or "center".
Examples
--------
@ -144,50 +144,37 @@ class Paragraph(VGroup):
"""
def __init__(self, *text, line_spacing=-1, alignment=None, **config):
def __init__(
self,
*text: Sequence[str],
line_spacing: float = -1,
alignment: Optional[str] = None,
**kwargs,
) -> None:
self.line_spacing = line_spacing
self.alignment = alignment
self.consider_spaces_as_chars = kwargs.get("disable_ligatures", False)
super().__init__()
lines_str = "\n".join(list(text))
self.lines_text = Text(lines_str, line_spacing=line_spacing, **config)
self.lines_text = Text(lines_str, line_spacing=line_spacing, **kwargs)
lines_str_list = lines_str.split("\n")
self.chars = self._gen_chars(lines_str_list)
chars_lines_text_list = self.get_group_class()()
char_index_counter = 0
for line_index in range(lines_str_list.__len__()):
chars_lines_text_list.add(
self.lines_text[
char_index_counter : char_index_counter
+ lines_str_list[line_index].__len__()
+ 1
],
)
char_index_counter += lines_str_list[line_index].__len__() + 1
self.lines = []
self.lines.append([])
for line_no in range(chars_lines_text_list.__len__()):
self.lines[0].append(chars_lines_text_list[line_no])
self.lines_initial_positions = []
for line_no in range(self.lines[0].__len__()):
self.lines_initial_positions.append(self.lines[0][line_no].get_center())
self.lines.append([])
self.lines[1].extend(
[self.alignment for _ in range(chars_lines_text_list.__len__())],
)
self.lines = [list(self.chars), [self.alignment] * len(self.chars)]
self.lines_initial_positions = [line.get_center() for line in self.lines[0]]
self.add(*self.lines[0])
self.move_to(np.array([0, 0, 0]))
if self.alignment:
self._set_all_lines_alignments(self.alignment)
def _gen_chars(self, lines_str_list):
"""Function to convert plain string to 2d-VGroup of chars. 2d-VGroup mean "VGroup of VGroup".
def _gen_chars(self, lines_str_list: list) -> VGroup:
"""Function to convert a list of plain strings to a VGroup of VGroups of chars.
Parameters
----------
lines_str_list : :class:`str`
Plain text string.
lines_str_list
List of plain text strings.
Returns
-------
@ -196,72 +183,85 @@ class Paragraph(VGroup):
"""
char_index_counter = 0
chars = self.get_group_class()()
for line_no in range(lines_str_list.__len__()):
for line_no in range(len(lines_str_list)):
line_str = lines_str_list[line_no]
# Count all the characters in line_str
# Spaces may or may not count as characters
if self.consider_spaces_as_chars:
char_count = len(line_str)
else:
char_count = 0
for char in line_str:
if not char.isspace():
char_count += 1
chars.add(self.get_group_class()())
chars[line_no].add(
*self.lines_text.chars[
char_index_counter : char_index_counter
+ lines_str_list[line_no].__len__()
+ 1
char_index_counter : char_index_counter + char_count
]
)
char_index_counter += lines_str_list[line_no].__len__() + 1
char_index_counter += char_count
if self.consider_spaces_as_chars:
# If spaces count as characters, count the extra \n character
# which separates Paragraph's lines to avoid issues
char_index_counter += 1
return chars
def _set_all_lines_alignments(self, alignment):
def _set_all_lines_alignments(self, alignment: str) -> Paragraph:
"""Function to set all line's alignment to a specific value.
Parameters
----------
alignment : :class:`str`
alignment
Defines the alignment of paragraph. Possible values are "left", "right", "center".
"""
for line_no in range(0, self.lines[0].__len__()):
for line_no in range(len(self.lines[0])):
self._change_alignment_for_a_line(alignment, line_no)
return self
def _set_line_alignment(self, alignment, line_no):
def _set_line_alignment(self, alignment: str, line_no: int) -> Paragraph:
"""Function to set one line's alignment to a specific value.
Parameters
----------
alignment : :class:`str`
alignment
Defines the alignment of paragraph. Possible values are "left", "right", "center".
line_no : :class:`int`
line_no
Defines the line number for which we want to set given alignment.
"""
self._change_alignment_for_a_line(alignment, line_no)
return self
def _set_all_lines_to_initial_positions(self):
def _set_all_lines_to_initial_positions(self) -> Paragraph:
"""Set all lines to their initial positions."""
self.lines[1] = [None for _ in range(self.lines[0].__len__())]
for line_no in range(0, self.lines[0].__len__()):
self.lines[1] = [None] * len(self.lines[0])
for line_no in range(len(self.lines[0])):
self[line_no].move_to(
self.get_center() + self.lines_initial_positions[line_no],
)
return self
def _set_line_to_initial_position(self, line_no):
def _set_line_to_initial_position(self, line_no: int) -> Paragraph:
"""Function to set one line to initial positions.
Parameters
----------
line_no : :class:`int`
line_no
Defines the line number for which we want to set given alignment.
"""
self.lines[1][line_no] = None
self[line_no].move_to(self.get_center() + self.lines_initial_positions[line_no])
return self
def _change_alignment_for_a_line(self, alignment, line_no):
def _change_alignment_for_a_line(self, alignment: str, line_no: int) -> None:
"""Function to change one line's alignment to a specific value.
Parameters
----------
alignment : :class:`str`
alignment
Defines the alignment of paragraph. Possible values are "left", "right", "center".
line_no : :class:`int`
line_no
Defines the line number for which we want to set given alignment.
"""
self.lines[1][line_no] = alignment
@ -299,13 +299,13 @@ class Text(SVGMobject):
Parameters
----------
text : :class:`str`
The text that need to created as mobject.
text
The text that needs to be created as a mobject.
Returns
-------
:class:`Text`
The mobject like :class:`.VGroup`.
The mobject-like :class:`.VGroup`.
Examples
---------
@ -426,7 +426,7 @@ class Text(SVGMobject):
unpack_groups: bool = True,
disable_ligatures: bool = False,
**kwargs,
):
) -> None:
self.line_spacing = line_spacing
self.font = font
@ -540,8 +540,8 @@ class Text(SVGMobject):
def _gen_chars(self):
chars = self.get_group_class()()
submobjects_char_index = 0
for char_index in range(self.text.__len__()):
if self.text[char_index] in (" ", "\t", "\n"):
for char_index in range(len(self.text)):
if self.text[char_index].isspace():
space = Dot(radius=0, fill_opacity=0, stroke_opacity=0)
if char_index == 0:
space.move_to(self.submobjects[submobjects_char_index].get_center())
@ -895,23 +895,23 @@ class MarkupText(SVGMobject):
Parameters
----------
text : :class:`str`
The text that need to created as mobject.
fill_opacity : :class:`int`
The fill opacity with 1 meaning opaque and 0 meaning transparent.
stroke_width : :class:`int`
text
The text that needs to be created as mobject.
fill_opacity
The fill opacity, with 1 meaning opaque and 0 meaning transparent.
stroke_width
Stroke width.
font_size : :class:`float`
font_size
Font size.
line_spacing : :class:`int`
line_spacing
Line spacing.
font : :class:`str`
font
Global font setting for the entire text. Local overrides are possible.
slant : :class:`str`
slant
Global slant setting, e.g. `NORMAL` or `ITALIC`. Local overrides are possible.
weight : :class:`str`
weight
Global weight setting, e.g. `NORMAL` or `BOLD`. Local overrides are possible.
gradient: :class:`tuple`
gradient
Global gradient setting. Local overrides are possible.
@ -1107,7 +1107,7 @@ class MarkupText(SVGMobject):
unpack_groups: bool = True,
disable_ligatures: bool = False,
**kwargs,
):
) -> None:
self.text = text
self.line_spacing = line_spacing
@ -1385,7 +1385,7 @@ def register_font(font_file: str | Path):
Parameters
----------
font_file :
font_file
The font file to add.
Examples