Skip to content

Commit 90a6aad

Browse files
newellpre-commit-ci[bot]jsonvillanuevamarkromanmiller
authored
Add SVG <line> element support to :class:~.SVGMobject (#1275)
* Add SVG <line> element support * Add support for SVG <line> elements to SVGMobject * Add unit test, unit test SVG file, and unit test data file * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jason Villanueva <[email protected]> Co-authored-by: Mark Miller <[email protected]>
1 parent 5017a49 commit 90a6aad

File tree

4 files changed

+101
-65
lines changed

4 files changed

+101
-65
lines changed

manim/mobject/svg/svg_mobject.py

Lines changed: 91 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from ... import config
1919
from ...constants import *
20-
from ...mobject.geometry import Circle, Rectangle, RoundedRectangle
20+
from ...mobject.geometry import Circle, Line, Rectangle, RoundedRectangle
2121
from ...mobject.types.vectorized_mobject import VGroup, VMobject
2222
from .style_utils import cascade_element_style, parse_style
2323
from .svg_path import SVGPathMobject, string_to_numbers
@@ -183,6 +183,8 @@ def get_mobjects_from(
183183
elif element.tagName == "use":
184184
# note, style is calcuated in a different way for `use` elements.
185185
result += self.use_to_mobjects(element, style)
186+
elif element.tagName in ["line"]:
187+
result.append(self.line_to_mobject(element, style))
186188
elif element.tagName == "rect":
187189
result.append(self.rect_to_mobject(element, style))
188190
elif element.tagName == "circle":
@@ -227,6 +229,24 @@ def path_string_to_mobject(self, path_string: str, style: dict):
227229
"""
228230
return SVGPathMobject(path_string, **parse_style(style))
229231

232+
def attribute_to_float(self, attr):
233+
"""A helper method which converts the attribute to float.
234+
235+
Parameters
236+
----------
237+
attr : str
238+
An SVG path attribute.
239+
240+
Returns
241+
-------
242+
float
243+
A float representing the attribute string value.
244+
"""
245+
stripped_attr = "".join(
246+
[char for char in attr if char in string.digits + "." + "-"]
247+
)
248+
return float(stripped_attr)
249+
230250
def use_to_mobjects(
231251
self, use_element: MinidomElement, local_style: Dict
232252
) -> List[VMobject]:
@@ -266,48 +286,78 @@ def use_to_mobjects(
266286

267287
return self.get_mobjects_from(def_element, style)
268288

269-
def attribute_to_float(self, attr):
270-
"""A helper method which converts the attribute to float.
289+
def line_to_mobject(self, line_element: MinidomElement, style: dict):
290+
"""Creates a Line VMobject from an SVG <line> element.
271291
272292
Parameters
273293
----------
274-
attr : str
275-
An SVG path attribute.
294+
line_element : :class:`minidom.Element`
295+
An SVG line element.
296+
297+
style : :class:`dict`
298+
Style specification, using the SVG names for properties.
276299
277300
Returns
278301
-------
279-
float
280-
A float representing the attribute string value.
302+
Line
303+
A Line VMobject
281304
"""
282-
stripped_attr = "".join(
283-
[char for char in attr if char in string.digits + "." + "-"]
284-
)
285-
return float(stripped_attr)
305+
x1, y1, x2, y2 = [
306+
self.attribute_to_float(line_element.getAttribute(key))
307+
if line_element.hasAttribute(key)
308+
else 0.0
309+
for key in ("x1", "y1", "x2", "y2")
310+
]
311+
return Line([x1, -y1, 0], [x2, -y2, 0], **parse_style(style))
286312

287-
def polygon_to_mobject(self, polygon_element: MinidomElement, style: dict):
288-
"""Constructs a VMobject from a SVG <polygon> element.
313+
def rect_to_mobject(self, rect_element: MinidomElement, style: dict):
314+
"""Converts a SVG <rect> command to a VMobject.
289315
290316
Parameters
291317
----------
292-
polygon_element : :class:`minidom.Element`
293-
An SVG polygon element.
318+
rect_element : minidom.Element
319+
A SVG rect path command.
294320
295-
style : :class:`dict`
321+
style : dict
296322
Style specification, using the SVG names for properties.
297323
298324
Returns
299325
-------
300-
VMobjectFromSVGPathstring
301-
A VMobject representing the polygon.
326+
Rectangle
327+
Creates either a Rectangle, or RoundRectangle, VMobject from a
328+
rect element.
302329
"""
303-
# This seems hacky... yes it is.
304-
path_string = polygon_element.getAttribute("points").lstrip()
305-
for digit in string.digits:
306-
path_string = path_string.replace(" " + digit, " L" + digit)
307-
path_string = "M" + path_string
308-
if polygon_element.tagName == "polygon":
309-
path_string = path_string + "Z"
310-
return self.path_string_to_mobject(path_string, style)
330+
331+
stroke_width = rect_element.getAttribute("stroke-width")
332+
corner_radius = rect_element.getAttribute("rx")
333+
334+
if stroke_width in ["", "none", "0"]:
335+
stroke_width = 0
336+
337+
if corner_radius in ["", "0", "none"]:
338+
corner_radius = 0
339+
340+
corner_radius = float(corner_radius)
341+
342+
parsed_style = parse_style(style)
343+
parsed_style["stroke_width"] = stroke_width
344+
345+
if corner_radius == 0:
346+
mob = Rectangle(
347+
width=self.attribute_to_float(rect_element.getAttribute("width")),
348+
height=self.attribute_to_float(rect_element.getAttribute("height")),
349+
**parsed_style,
350+
)
351+
else:
352+
mob = RoundedRectangle(
353+
width=self.attribute_to_float(rect_element.getAttribute("width")),
354+
height=self.attribute_to_float(rect_element.getAttribute("height")),
355+
corner_radius=corner_radius,
356+
**parsed_style,
357+
)
358+
359+
mob.shift(mob.get_center() - mob.get_corner(UP + LEFT))
360+
return mob
311361

312362
def circle_to_mobject(self, circle_element: MinidomElement, style: dict):
313363
"""Creates a Circle VMobject from a SVG <circle> command.
@@ -362,54 +412,30 @@ def ellipse_to_mobject(self, circle_element: MinidomElement, style: dict):
362412
.shift(x * RIGHT + y * DOWN)
363413
)
364414

365-
def rect_to_mobject(self, rect_element: MinidomElement, style: dict):
366-
"""Converts a SVG <rect> command to a VMobject.
415+
def polygon_to_mobject(self, polygon_element: MinidomElement, style: dict):
416+
"""Constructs a VMobject from a SVG <polygon> element.
367417
368418
Parameters
369419
----------
370-
rect_element : minidom.Element
371-
A SVG rect path command.
420+
polygon_element : :class:`minidom.Element`
421+
An SVG polygon element.
372422
373-
style : dict
423+
style : :class:`dict`
374424
Style specification, using the SVG names for properties.
375425
376426
Returns
377427
-------
378-
Rectangle
379-
Creates either a Rectangle, or RoundRectangle, VMobject from a
380-
rect element.
428+
VMobjectFromSVGPathstring
429+
A VMobject representing the polygon.
381430
"""
382-
383-
stroke_width = rect_element.getAttribute("stroke-width")
384-
corner_radius = rect_element.getAttribute("rx")
385-
386-
if stroke_width in ["", "none", "0"]:
387-
stroke_width = 0
388-
389-
if corner_radius in ["", "0", "none"]:
390-
corner_radius = 0
391-
392-
corner_radius = float(corner_radius)
393-
394-
parsed_style = parse_style(style)
395-
parsed_style["stroke_width"] = stroke_width
396-
397-
if corner_radius == 0:
398-
mob = Rectangle(
399-
width=self.attribute_to_float(rect_element.getAttribute("width")),
400-
height=self.attribute_to_float(rect_element.getAttribute("height")),
401-
**parsed_style,
402-
)
403-
else:
404-
mob = RoundedRectangle(
405-
width=self.attribute_to_float(rect_element.getAttribute("width")),
406-
height=self.attribute_to_float(rect_element.getAttribute("height")),
407-
corner_radius=corner_radius,
408-
**parsed_style,
409-
)
410-
411-
mob.shift(mob.get_center() - mob.get_corner(UP + LEFT))
412-
return mob
431+
# This seems hacky... yes it is.
432+
path_string = polygon_element.getAttribute("points").lstrip()
433+
for digit in string.digits:
434+
path_string = path_string.replace(" " + digit, " L" + digit)
435+
path_string = "M" + path_string
436+
if polygon_element.tagName == "polygon":
437+
path_string = path_string + "Z"
438+
return self.path_string_to_mobject(path_string, style)
413439

414440
def handle_transforms(self, element, mobject):
415441
"""Applies the SVG transform to the specified mobject. Transforms include:
Binary file not shown.
Lines changed: 3 additions & 0 deletions
Loading

tests/test_graphical_units/test_img_and_svg.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ def get_test_resource(filename):
2323
# First are the simple tests.
2424

2525

26+
class LineTest(Scene):
27+
def construct(self):
28+
line_demo = SVGMobject(get_test_resource("line.svg"))
29+
self.add(line_demo)
30+
self.wait()
31+
32+
2633
class CubicPathTest(Scene):
2734
def construct(self):
2835
cubic_demo = SVGMobject(get_test_resource("cubic_demo.svg"))

0 commit comments

Comments
 (0)