summaryrefslogtreecommitdiffstats
path: root/share/extensions/inkex/paths.py
diff options
context:
space:
mode:
Diffstat (limited to 'share/extensions/inkex/paths.py')
-rw-r--r--share/extensions/inkex/paths.py1547
1 files changed, 1547 insertions, 0 deletions
diff --git a/share/extensions/inkex/paths.py b/share/extensions/inkex/paths.py
new file mode 100644
index 0000000..b3620e8
--- /dev/null
+++ b/share/extensions/inkex/paths.py
@@ -0,0 +1,1547 @@
+# coding=utf-8
+#
+# Copyright (C) 2018 Martin Owens <doctormo@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+"""
+functions for digesting paths into a simple list structure
+"""
+
+import re
+import copy
+
+from math import atan2, cos, pi, sin, sqrt, acos, tan
+
+from .transforms import Transform, BoundingBox, Vector2d
+from .utils import classproperty, strargs
+
+try: # pylint: disable=using-constant-test
+ from typing import overload, Any, Type, Dict, Optional, Union, Tuple, List, Iterator, Generator # pylint: disable=unused-import
+ from typing import TypeVar
+ Pathlike = TypeVar('Pathlike', bound="PathCommand")
+ AbsolutePathlike = TypeVar('AbsolutePathlike', bound="AbsolutePathCommand")
+except ImportError:
+ overload = lambda x: x
+
+# All the names that get added to the inkex API itself.
+__all__ = (
+ 'Path', 'CubicSuperPath',
+ # Path commands:
+ 'Line', 'line',
+ 'Move', 'move',
+ 'ZoneClose', 'zoneClose',
+ 'Horz', 'horz',
+ 'Vert', 'vert',
+ 'Curve', 'curve',
+ 'Smooth', 'smooth',
+ 'Quadratic', 'quadratic',
+ 'TepidQuadratic', 'tepidQuadratic',
+ 'Arc', 'arc',
+ # errors
+ 'InvalidPath'
+)
+
+LEX_REX = re.compile(r'([MLHVCSQTAZmlhvcsqtaz])([^MLHVCSQTAZmlhvcsqtaz]*)')
+NONE = lambda obj: obj is not None
+
+
+class InvalidPath(ValueError):
+ """Raised when given an invalid path string"""
+
+
+class PathCommand(object):
+ """
+ Base class of all path commands
+ """
+
+ # Number of arguments that follow this path commands letter
+ nargs = -1
+
+ # The full name of the segment (i.e. Line, Arc, etc)
+ name = classproperty(lambda cls: cls.__name__)
+
+ # The single letter representation of this command (i.e. L, A, etc)
+ letter = classproperty(lambda cls: cls.name[0])
+
+ # The implicit next command. This is for automatic chains where the next command
+ # isn't given, just a bunch on numbers which we automatically parse.
+ @classproperty
+ def next_command(self):
+ return self
+
+ @property
+ def is_relative(self): # type: () -> bool
+ raise NotImplementedError
+
+ @property
+ def is_absolute(self): # type: () -> bool
+ raise NotImplementedError
+
+ def to_relative(self, prev): # type: (Vector2d) -> RelativePathCommand
+ """Return absolute counterpart for absolute commands or copy for relative"""
+ raise NotImplementedError
+
+ def to_absolute(self, prev): # type: (Vector2d) -> AbsolutePathCommand
+ """Return relative counterpart for relative commands or copy for absolute"""
+ raise NotImplementedError
+
+ # The precision of the numbers when converting to string
+ number_template = "{:.6g}"
+
+ # Maps single letter path command to corresponding class
+ # (filled at the bottom of file, when all classes already defined)
+ _letter_to_class = {} # type: Dict[str, Type[Any]]
+
+ @staticmethod
+ def letter_to_class(letter):
+ """Returns class for given path command letter"""
+ return PathCommand._letter_to_class[letter]
+
+ @property
+ def args(self): # type: () -> List[float]
+ """Returns path command arguments as tuple of floats"""
+ raise NotImplementedError()
+
+ def control_points(self, first, prev, prev_prev):
+ # type: (Vector2d, Vector2d, Vector2d) -> Union[List[Vector2d], Generator[Vector2d, None, None]]
+ """Returns list of path command control points"""
+ raise NotImplementedError
+
+ @classmethod
+ def _argt(cls, sep):
+ return sep.join([cls.number_template] * cls.nargs)
+
+ def __str__(self):
+ return "{} {}".format(self.letter, self._argt(" ").format(*self.args)).strip()
+
+ def __repr__(self):
+ return "{{}}({})".format(self._argt(", ")).format(self.name, *self.args)
+
+ def __eq__(self, other):
+ previous = Vector2d()
+ if type(self) == type(other): # pylint: disable=unidiomatic-typecheck
+ return self.args == other.args
+ if isinstance(other, tuple):
+ return self.args == other
+ if not isinstance(other, PathCommand):
+ raise ValueError("Can't compare types")
+ try:
+ if self.is_relative == other.is_relative:
+ return self.to_curve(previous) == other.to_curve(previous)
+ except ValueError:
+ pass
+ return False
+
+ def end_point(self, first, prev): # type: (Vector2d, Vector2d) -> Vector2d
+ """Returns last control point of path command"""
+ raise NotImplementedError()
+
+ def update_bounding_box(self, first, last_two_points, bbox):
+ # type: (Vector2d, List[Vector2d], BoundingBox) -> None
+ # pylint: disable=unused-argument
+ """
+ Enlarges given bbox to contain path element.
+
+ :param (tuple of float) first: first point of path. Required to calculate Z segment
+ :param (list of tuple) last_two_points: list with last two control points in abs coords.
+ :param (BoundingBox) bbox: bounding box to update
+ """
+ raise NotImplementedError("Bounding box is not implemented for {}".format(self.name))
+
+ def to_curve(self, prev, prev_prev=Vector2d()):
+ # type: (Vector2d, Vector2d) -> Curve
+ """Convert command to :py:class:`Curve`
+ Curve().to_curve() returns a copy
+ """
+ raise NotImplementedError("To curve not supported for {}".format(self.name))
+
+ def to_curves(self, prev, prev_prev=Vector2d()):
+ # type: (Vector2d, Vector2d) -> List[Curve]
+ """Convert command to list of :py:class:`Curve` commands """
+ return [self.to_curve(prev, prev_prev)]
+
+ def to_line(self, prev):
+ # type: (Vector2d) -> Line
+ """Converts this segment to a line (copies if already a line)"""
+ return Line(*self.end_point(Vector2d(), prev))
+
+
+class RelativePathCommand(PathCommand):
+ """
+ Abstract base class for relative path commands.
+
+ Implements most of methods of :py:class:`PathCommand` through
+ conversion to :py:class:`AbsolutePathCommand`
+ """
+
+ @property
+ def is_relative(self):
+ return True
+
+ @property
+ def is_absolute(self):
+ return False
+
+ def control_points(self, first, prev, prev_prev):
+ # type: (Vector2d, Vector2d, Vector2d) -> Union[List[Vector2d], Generator[Vector2d, None, None]]
+ return self.to_absolute(prev).control_points(first, prev, prev_prev)
+
+ def to_relative(self, prev):
+ # type: (Pathlike, Vector2d) -> Pathlike
+ return self.__class__(*self.args)
+
+ def update_bounding_box(self, first, last_two_points, bbox):
+ self.to_absolute(last_two_points[-1]).update_bounding_box(first, last_two_points, bbox)
+
+ def end_point(self, first, prev):
+ # type: (Vector2d, Vector2d) -> Vector2d
+ return self.to_absolute(prev).end_point(first, prev)
+
+ def to_curve(self, prev, prev_prev=Vector2d()):
+ # type: (Vector2d, Vector2d) -> Curve
+ return self.to_absolute(prev).to_curve(prev, prev_prev)
+
+ def to_curves(self, prev, prev_prev=Vector2d()):
+ # type: (Vector2d, Vector2d) -> List[Curve]
+ return self.to_absolute(prev).to_curves(prev, prev_prev)
+
+
+class AbsolutePathCommand(PathCommand):
+ """
+ Absolute path command. Unlike :py:class:`RelativePathCommand` can be transformed directly.
+ """
+
+ @property
+ def is_relative(self):
+ return False
+
+ @property
+ def is_absolute(self):
+ return True
+
+ def to_absolute(self, previous): # type: (AbsolutePathlike, Vector2d) -> AbsolutePathlike
+ return self.__class__(*self.args)
+
+ def transform(self, transform): # type: (AbsolutePathlike, Transform) -> AbsolutePathlike
+ """Returns new transformed segment
+
+ :param transform: a transformation to apply
+ """
+ raise NotImplementedError()
+
+ def rotate(self, degrees, center): # type: (AbsolutePathlike, float, Vector2d) -> AbsolutePathlike
+ """
+ Returns new transformed segment
+
+ :param degrees: rotation angle in degrees
+ :param center: invariant point of rotation
+ """
+ return self.transform(Transform(rotate=(degrees, center[0], center[1])))
+
+ def translate(self, dr): # type: (AbsolutePathlike, Vector2d) -> AbsolutePathlike
+ """Translate or scale this path command by dr"""
+ return self.transform(Transform(translate=dr))
+
+ def scale(self, factor): # type: (AbsolutePathlike, Union[float, Tuple[float,float]]) -> AbsolutePathlike
+ """Returns new transformed segment
+
+ :param factor: scale or (scale_x, scale_y)
+ """
+ return self.transform(Transform(scale=factor))
+
+
+class Line(AbsolutePathCommand):
+ """Line segment"""
+
+ nargs = 2
+
+ @property
+ def args(self):
+ return self.x, self.y
+
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+
+ def update_bounding_box(self, first, last_two_points, bbox):
+ bbox += BoundingBox((last_two_points[-1].x, self.x), (last_two_points[-1].y, self.y))
+
+ def control_points(self, first, prev, prev_prev):
+ # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
+ yield Vector2d(self.x, self.y)
+
+ def to_relative(self, prev):
+ # type: (Vector2d) -> line
+ return line(self.x - prev.x, self.y - prev.y)
+
+ def transform(self, transform):
+ # type: (Line, Transform) -> Line
+ return Line(*transform.apply_to_point((self.x, self.y)))
+
+ def end_point(self, first, prev):
+ # type: (Vector2d, Vector2d) -> Vector2d
+ return Vector2d(self.x, self.y)
+
+ def to_curve(self, prev, prev_prev=Vector2d()):
+ # type: (Vector2d, Optional[Vector2d]) -> Curve
+ return Curve(prev.x, prev.y, self.x, self.y, self.x, self.y)
+
+
+
+class line(RelativePathCommand): # pylint: disable=invalid-name
+ """Relative line segment"""
+
+ nargs = 2
+
+ @property
+ def args(self):
+ return self.dx, self.dy
+
+ def __init__(self, dx, dy):
+ self.dx = dx
+ self.dy = dy
+
+ def to_absolute(self, prev): # type: (Vector2d) -> Line
+ return Line(prev.x + self.dx, prev.y + self.dy)
+
+
+class Move(AbsolutePathCommand):
+ """Move pen segment without a line"""
+
+ nargs = 2
+ next_command = Line
+
+ @property
+ def args(self):
+ return self.x, self.y
+
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+
+ def update_bounding_box(self, first, last_two_points, bbox):
+ bbox += BoundingBox(self.x, self.y)
+
+ def control_points(self, first, prev, prev_prev):
+ # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
+ yield Vector2d(self.x, self.y)
+
+ def to_relative(self, prev):
+ # type: (Vector2d) -> move
+ return move(self.x - prev.x, self.y - prev.y)
+
+ def transform(self, transform):
+ # type: (Transform) -> Move
+ return Move(*transform.apply_to_point((self.x, self.y)))
+
+ def end_point(self, first, prev):
+ # type: (Vector2d, Vector2d) -> Vector2d
+ return Vector2d(self.x, self.y)
+
+ def to_curve(self, prev, prev_prev=Vector2d()):
+ # type: (Vector2d, Optional[Vector2d]) -> Curve
+ raise ValueError("Move segments can not be changed into curves.")
+
+
+class move(RelativePathCommand): # pylint: disable=invalid-name
+ """Relative move segment"""
+
+ nargs = 2
+ next_command = line
+
+ @property
+ def args(self):
+ return self.dx, self.dy
+
+ def __init__(self, dx, dy):
+ self.dx = dx
+ self.dy = dy
+
+ def to_absolute(self, prev): # type: (Vector2d) -> Move
+ return Move(prev.x + self.dx, prev.y + self.dy)
+
+
+class ZoneClose(AbsolutePathCommand):
+ """Close segment to finish a path"""
+ nargs = 0
+ next_command = Move
+
+ @property
+ def args(self):
+ return ()
+
+ def update_bounding_box(self, first, last_two_points, bbox):
+ pass
+
+ def transform(self, transform):
+ # type: (Transform) -> ZoneClose
+ return ZoneClose()
+
+ def control_points(self, first, prev, prev_prev):
+ # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
+ yield first
+
+ def to_relative(self, prev):
+ # type: (Vector2d) -> zoneClose
+ return zoneClose()
+
+ def end_point(self, first, prev):
+ # type: (Vector2d, Vector2d) -> Vector2d
+ return first
+
+ def to_curve(self, prev, prev_prev=Vector2d()):
+ # type: (Vector2d, Optional[Vector2d]) -> Curve
+ raise ValueError("ZoneClose segments can not be changed into curves.")
+
+
+class zoneClose(RelativePathCommand): # pylint: disable=invalid-name
+ """Same as above (svg says no difference)"""
+
+ nargs = 0
+ next_command = Move
+
+ @property
+ def args(self):
+ return ()
+
+ def to_absolute(self, prev):
+ return ZoneClose()
+
+
+class Horz(AbsolutePathCommand):
+ """Horizontal Line segment"""
+ nargs = 1
+
+ @property
+ def args(self):
+ return self.x,
+
+ def __init__(self, x):
+ self.x = x
+
+ def update_bounding_box(self, first, last_two_points, bbox):
+ bbox += BoundingBox((last_two_points[-1].x, self.x), last_two_points[-1].y)
+
+ def control_points(self, first, prev, prev_prev):
+ # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
+ yield Vector2d(self.x, prev.y)
+
+ def to_relative(self, prev):
+ # type: (Vector2d) -> horz
+ return horz(self.x - prev.x)
+
+ def transform(self, transformation):
+ # type: (Pathlike, Transform) -> Pathlike
+ raise ValueError("Horizontal lines can't be transformed directly.")
+
+ def end_point(self, first, prev):
+ # type: (Vector2d, Vector2d) -> Vector2d
+ return Vector2d(self.x, prev.y)
+
+ def to_curve(self, prev, prev_prev=Vector2d()):
+ # type: (Vector2d, Optional[Vector2d]) -> Curve
+ """Convert a horizontal line into a curve"""
+ return self.to_line(prev).to_curve(prev)
+
+ def to_line(self, prev):
+ # type: (Vector2d) -> Line
+ """Return this path command as a Line instead"""
+ return Line(self.x, prev.y)
+
+
+class horz(RelativePathCommand): # pylint: disable=invalid-name
+ """Relative horz line segment"""
+
+ nargs = 1
+
+ @property
+ def args(self):
+ return self.dx,
+
+ def __init__(self, dx):
+ self.dx = dx
+
+ def to_absolute(self, prev): # type: (Vector2d) -> Horz
+ return Horz(prev.x + self.dx)
+
+ def to_line(self, prev): # type: (Vector2d) -> Line
+ """Return this path command as a Line instead"""
+ return Line(prev.x + self.dx, prev.y)
+
+
+class Vert(AbsolutePathCommand):
+ """Vertical Line segment"""
+
+ nargs = 1
+
+ @property
+ def args(self):
+ return self.y,
+
+ def __init__(self, y):
+ self.y = y
+
+ def update_bounding_box(self, first, last_two_points, bbox):
+ bbox += BoundingBox(last_two_points[-1].x, (last_two_points[-1].y, self.y))
+
+ def transform(self, transform): # type: (Pathlike, Transform) -> Pathlike
+ raise ValueError("Vertical lines can't be transformed directly.")
+
+ def control_points(self, first, prev, prev_prev):
+ # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
+ yield Vector2d(prev.x, self.y)
+
+ def to_relative(self, prev):
+ # type: (Vector2d) -> vert
+ return vert(self.y - prev.y)
+
+ def end_point(self, first, prev):
+ # type: (Vector2d, Vector2d) -> Vector2d
+ return Vector2d(prev.x, self.y)
+
+ def to_line(self, prev):
+ # type: (Vector2d) -> Line
+ """Return this path command as a line instead"""
+ return Line(prev.x, self.y)
+
+ def to_curve(self, prev, prev_prev=Vector2d()): # type: (Vector2d, Optional[Vector2d]) -> Curve
+ """Convert a horizontal line into a curve"""
+ return self.to_line(prev).to_curve(prev)
+
+
+class vert(RelativePathCommand): # pylint: disable=invalid-name
+ """Relative vertical line segment"""
+
+ nargs = 1
+
+ @property
+ def args(self):
+ return self.dy,
+
+ def __init__(self, dy):
+ self.dy = dy
+
+ def to_absolute(self, prev): # type: (Vector2d) -> Vert
+ return Vert(prev.y + self.dy)
+
+ def to_line(self, prev): # type: (Vector2d) -> Line
+ """Return this path command as a line instead"""
+ return Line(prev.x, prev.y + self.dy)
+
+
+class Curve(AbsolutePathCommand):
+ """Absolute Curved Line segment"""
+ nargs = 6
+
+ @property
+ def args(self):
+ return self.x2, self.y2, self.x3, self.y3, self.x4, self.y4
+
+ def __init__(self, x2, y2, x3, y3, x4, y4):
+ self.x2 = x2
+ self.y2 = y2
+
+ self.x3 = x3
+ self.y3 = y3
+
+ self.x4 = x4
+ self.y4 = y4
+
+ def update_bounding_box(self, first, last_two_points, bbox):
+ from .transforms import cubic_extrema
+
+ x1, x2, x3, x4 = last_two_points[-1].x, self.x2, self.x3, self.x4
+ y1, y2, y3, y4 = last_two_points[-1].y, self.y2, self.y3, self.y4
+
+ if not (x1 in bbox.x and
+ x2 in bbox.x and
+ x3 in bbox.x and
+ x4 in bbox.x):
+ bbox.x += cubic_extrema(x1, x2, x3, x4)
+
+ if not (y1 in bbox.y and
+ y2 in bbox.y and
+ y3 in bbox.y and
+ y4 in bbox.y):
+ bbox.y += cubic_extrema(y1, y2, y3, y4)
+
+ def transform(self, transform):
+ # type: (Transform) -> Curve
+ x2, y2 = transform.apply_to_point((self.x2, self.y2))
+ x3, y3 = transform.apply_to_point((self.x3, self.y3))
+ x4, y4 = transform.apply_to_point((self.x4, self.y4))
+ return Curve(x2, y2, x3, y3, x4, y4)
+
+ def control_points(self, first, prev, prev_prev):
+ # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
+ yield Vector2d(self.x2, self.y2)
+ yield Vector2d(self.x3, self.y3)
+ yield Vector2d(self.x4, self.y4)
+
+ def to_relative(self, prev): # type: (Vector2d) -> curve
+ return curve(
+ self.x2 - prev.x, self.y2 - prev.y,
+ self.x3 - prev.x, self.y3 - prev.y,
+ self.x4 - prev.x, self.y4 - prev.y
+ )
+
+ def end_point(self, first, prev):
+ return Vector2d(self.x4, self.y4)
+
+ def to_curve(self, prev, prev_prev=Vector2d()): # type: (Vector2d, Optional[Vector2d]) -> Curve
+ """No conversion needed, pass-through, returns self"""
+ return Curve(*self.args)
+
+ def to_bez(self):
+ """Returns the list of coords for SuperPath"""
+ return [list(self.args[:2]), list(self.args[2:4]), list(self.args[4:6])]
+
+class curve(RelativePathCommand): # pylint: disable=invalid-name
+ """Relative curved line segment"""
+ nargs = 6
+
+ @property
+ def args(self):
+ return self.dx2, self.dy2, self.dx3, self.dy3, self.dx4, self.dy4
+
+ def __init__(self, dx2, dy2, dx3, dy3, dx4, dy4):
+ self.dx2 = dx2
+ self.dy2 = dy2
+
+ self.dx3 = dx3
+ self.dy3 = dy3
+
+ self.dx4 = dx4
+ self.dy4 = dy4
+
+ def to_absolute(self, prev): # type: (Vector2d) -> Curve
+ return Curve(
+ self.dx2 + prev.x, self.dy2 + prev.y,
+ self.dx3 + prev.x, self.dy3 + prev.y,
+ self.dx4 + prev.x, self.dy4 + prev.y
+ )
+
+
+class Smooth(AbsolutePathCommand):
+ """Absolute Smoothed Curved Line segment"""
+ nargs = 4
+
+ @property
+ def args(self):
+ return self.x3, self.y3, self.x4, self.y4
+
+ def __init__(self, x3, y3, x4, y4):
+
+ self.x3 = x3
+ self.y3 = y3
+
+ self.x4 = x4
+ self.y4 = y4
+
+ def update_bounding_box(self, first, last_two_points, bbox):
+ self.to_curve(last_two_points[-1], last_two_points[-2]).update_bounding_box(first, last_two_points, bbox)
+
+ def control_points(self, first, prev, prev_prev):
+ # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
+
+ x1, x2, x3, x4 = prev_prev.x, prev.x, self.x3, self.x4
+ y1, y2, y3, y4 = prev_prev.y, prev.y, self.y3, self.y4
+
+ # infer reflected point
+ x2 = 2 * x2 - x1
+ y2 = 2 * y2 - y1
+
+ yield Vector2d(x2, y2)
+ yield Vector2d(x3, y3)
+ yield Vector2d(x4, y4)
+
+ def to_relative(self, prev): # type: (Vector2d) -> smooth
+ return smooth(
+ self.x3 - prev.x, self.y3 - prev.y,
+ self.x4 - prev.x, self.y4 - prev.y
+ )
+
+ def transform(self, transform):
+ # type: (Transform) -> Smooth
+ x3, y3 = transform.apply_to_point((self.x3, self.y3))
+ x4, y4 = transform.apply_to_point((self.x4, self.y4))
+ return Smooth(x3, y3, x4, y4)
+
+ def end_point(self, first, prev):
+ return Vector2d(self.x4, self.y4)
+
+ def to_curve(self, prev, prev_prev=Vector2d()):
+ # type: (Vector2d, Vector2d) -> Curve
+ """
+ Convert this Smooth curve to a regular curve by creating a mirror
+ set of nodes based on the previous node. Previous should be a curve.
+ """
+ (x2, y2), (x3, y3), (x4, y4) = self.control_points(prev, prev, prev_prev)
+ return Curve(x2, y2, x3, y3, x4, y4)
+
+
+class smooth(RelativePathCommand): # pylint: disable=invalid-name
+ """Relative smoothed curved line segment"""
+ nargs = 4
+
+ @property
+ def args(self):
+ return self.dx3, self.dy3, self.dx4, self.dy4
+
+ def __init__(self, dx3, dy3, dx4, dy4):
+ self.dx3 = dx3
+ self.dy3 = dy3
+
+ self.dx4 = dx4
+ self.dy4 = dy4
+
+ def to_absolute(self, prev): # type: (Vector2d) -> Smooth
+ return Smooth(
+ self.dx3 + prev.x, self.dy3 + prev.y,
+ self.dx4 + prev.x, self.dy4 + prev.y
+ )
+
+
+class Quadratic(AbsolutePathCommand):
+ """Absolute Quadratic Curved Line segment"""
+ nargs = 4
+
+ @property
+ def args(self):
+ return self.x2, self.y2, self.x3, self.y3
+
+ def __init__(self, x2, y2, x3, y3):
+
+ self.x2 = x2
+ self.y2 = y2
+
+ self.x3 = x3
+ self.y3 = y3
+
+ def update_bounding_box(self, first, last_two_points, bbox):
+ from .transforms import quadratic_extrema
+
+ x1, x2, x3 = last_two_points[-1].x, self.x2, self.x3
+ y1, y2, y3 = last_two_points[-1].y, self.y2, self.y3
+
+ if not (x1 in bbox.x and
+ x2 in bbox.x and
+ x3 in bbox.x):
+ bbox.x += quadratic_extrema(x1, x2, x3)
+
+ if not (y1 in bbox.y and
+ y2 in bbox.y and
+ y3 in bbox.y):
+ bbox.y += quadratic_extrema(y1, y2, y3)
+
+ def control_points(self, first, prev, prev_prev):
+ # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
+ yield Vector2d(self.x2, self.y2)
+ yield Vector2d(self.x3, self.y3)
+
+ def to_relative(self, prev):
+ # type: (Vector2d) -> quadratic
+ return quadratic(
+ self.x2 - prev.x, self.y2 - prev.y,
+ self.x3 - prev.x, self.y3 - prev.y
+ )
+
+ def transform(self, transform):
+ # type: (Transform) -> Quadratic
+ x2, y2 = transform.apply_to_point((self.x2, self.y2))
+ x3, y3 = transform.apply_to_point((self.x3, self.y3))
+ return Quadratic(x2, y2, x3, y3)
+
+ def end_point(self, first, prev):
+ # type: (Vector2d, Vector2d) -> Vector2d
+ return Vector2d(self.x3, self.y3)
+
+ def to_curve(self, prev, prev_prev=Vector2d()):
+ # type: (Vector2d, Vector2d) -> Curve
+ """Attempt to convert a quadratic to a curve"""
+ prev = Vector2d(prev)
+ x1 = 1. / 3 * prev.x + 2. / 3 * self.x2
+ x2 = 2. / 3 * self.x2 + 1. / 3 * self.x3
+ y1 = 1. / 3 * prev.y + 2. / 3 * self.y2
+ y2 = 2. / 3 * self.y2 + 1. / 3 * self.y3
+ return Curve(x1, y1, x2, y2, self.x3, self.y3)
+
+
+class quadratic(RelativePathCommand): # pylint: disable=invalid-name
+ """Relative quadratic line segment"""
+ nargs = 4
+
+ @property
+ def args(self):
+ return self.dx2, self.dy2, self.dx3, self.dy3
+
+ def __init__(self, dx2, dy2, dx3, dy3):
+ self.dx2 = dx2
+ self.dx3 = dx3
+ self.dy2 = dy2
+ self.dy3 = dy3
+
+ def to_absolute(self, prev): # type: (Vector2d) -> Quadratic
+ return Quadratic(
+ self.dx2 + prev.x, self.dy2 + prev.y,
+ self.dx3 + prev.x, self.dy3 + prev.y
+ )
+
+
+class TepidQuadratic(AbsolutePathCommand):
+ """Continued Quadratic Line segment"""
+ nargs = 2
+
+ @property
+ def args(self):
+ return self.x3, self.y3
+
+ def __init__(self, x3, y3):
+ self.x3 = x3
+ self.y3 = y3
+
+ def update_bounding_box(self, first, last_two_points, bbox):
+ self.to_quadratic(last_two_points[-1], last_two_points[-2]).update_bounding_box(first, last_two_points, bbox)
+
+ def control_points(self, first, prev, prev_prev):
+ # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
+
+ x1, x2, x3 = prev_prev.x, prev.x, self.x3
+ y1, y2, y3 = prev_prev.y, prev.y, self.y3
+
+ # infer reflected point
+ x2 = 2 * x2 - x1
+ y2 = 2 * y2 - y1
+
+ yield Vector2d(x2, y2)
+ yield Vector2d(x3, y3)
+
+ def to_relative(self, prev): # type: (Vector2d) -> tepidQuadratic
+ return tepidQuadratic(
+ self.x3 - prev.x, self.y3 - prev.y
+ )
+
+ def transform(self, transform):
+ # type: (Transform) -> TepidQuadratic
+ x3, y3 = transform.apply_to_point((self.x3, self.y3))
+ return TepidQuadratic(x3, y3)
+
+ def end_point(self, first, prev):
+ # type: (Vector2d, Vector2d) -> Vector2d
+ return Vector2d(self.x3, self.y3)
+
+ def to_curve(self, prev, prev_prev=Vector2d()):
+ # type: (Vector2d, Vector2d) -> Curve
+ return self.to_quadratic(prev, prev_prev).to_curve(prev)
+
+ def to_quadratic(self, prev, prev_prev):
+ # type: (Vector2d, Vector2d) -> Quadratic
+ """
+ Convert this continued quadratic into a full quadratic
+ """
+ (x2, y2), (x3, y3) = self.control_points(prev, prev, prev_prev)
+ return Quadratic(x2, y2, x3, y3)
+
+
+class tepidQuadratic(RelativePathCommand): # pylint: disable=invalid-name
+ """Relative continued quadratic line segment"""
+ nargs = 2
+
+ @property
+ def args(self):
+ return self.dx3, self.dy3
+
+ def __init__(self, dx3, dy3):
+ self.dx3 = dx3
+ self.dy3 = dy3
+
+ def to_absolute(self, prev):
+ # type: (Vector2d) -> TepidQuadratic
+ return TepidQuadratic(
+ self.dx3 + prev.x, self.dy3 + prev.y
+ )
+
+
+class Arc(AbsolutePathCommand):
+ """Special Arc segment"""
+ nargs = 7
+
+ @property
+ def args(self):
+ return self.rx, self.ry, self.x_axis_rotation, self.large_arc, self.sweep, self.x, self.y
+
+ def __init__(self, rx, ry, x_axis_rotation, large_arc, sweep, x, y):
+ self.rx = rx
+ self.ry = ry
+ self.x_axis_rotation = x_axis_rotation
+ self.large_arc = large_arc
+ self.sweep = sweep
+ self.x = x
+ self.y = y
+
+ def update_bounding_box(self, first, last_two_points, bbox):
+ prev = last_two_points[-1]
+ for seg in self.to_curves(prev=prev):
+ seg.update_bounding_box(first, [None, prev], bbox)
+ prev = seg.end_point(first, prev)
+
+ def control_points(self, first, prev, prev_prev):
+ # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
+ yield Vector2d(self.x, self.y)
+
+ def to_curves(self, prev, prev_prev=Vector2d()):
+ # type: (Vector2d, Vector2d) -> List[Curve]
+ """Convert this arc into bezier curves"""
+ path = CubicSuperPath([arc_to_path(list(prev), self.args)]).to_path(curves_only=True)
+ # Ignore the first move command from to_path()
+ return list(path)[1:]
+
+ def transform(self, transform):
+ # type: (Transform) -> Arc
+ x_, y_ = transform.apply_to_point((self.x, self.y))
+
+ T = transform # type: Transform
+ if self.x_axis_rotation != 0:
+ T = T * Transform(rotate=self.x_axis_rotation)
+ a, c, b, d, _, _ = list(T.to_hexad())
+ # T = | a b |
+ # | c d |
+
+ detT = a * d - b * c
+ detT2 = detT ** 2
+
+ rx = float(self.rx)
+ ry = float(self.ry)
+
+ if rx == 0.0 or ry == 0.0 or detT2 == 0.0:
+ # invalid Arc parameters
+ # transform only last point
+ return Arc(self.rx, self.ry, self.x_axis_rotation, self.large_arc, self.sweep, x_, y_)
+
+ A = (d ** 2 / rx ** 2 + c ** 2 / ry ** 2) / detT2
+ B = - (d * b / rx ** 2 + c * a / ry ** 2) / detT2
+ D = (b ** 2 / rx ** 2 + a ** 2 / ry ** 2) / detT2
+
+ theta = atan2(-2 * B, D - A) / 2
+ theta_deg = theta * 180.0 / pi
+ DA = (D - A)
+ l2 = 4 * B ** 2 + DA ** 2
+
+ if l2 == 0:
+ delta = 0.0
+ else:
+ delta = 0.5 * (-DA ** 2 - 4 * B ** 2) / sqrt(l2)
+
+ half = (A + D) / 2
+
+ rx_ = 1.0 / sqrt(half + delta)
+ ry_ = 1.0 / sqrt(half - delta)
+
+ x_, y_ = transform.apply_to_point((self.x, self.y))
+
+ if detT > 0:
+ sweep = self.sweep
+ else:
+ sweep = 0 if self.sweep>0 else 1
+
+ return Arc(rx_, ry_, theta_deg, self.large_arc, sweep, x_, y_)
+
+ def to_relative(self, prev):
+ # type: (Vector2d) -> arc
+ return arc(self.rx, self.ry, self.x_axis_rotation, self.large_arc, self.sweep, self.x - prev.x, self.y - prev.y)
+
+ def end_point(self, first, prev):
+ # type: (Vector2d, Vector2d) -> Vector2d
+ return Vector2d(self.x, self.y)
+
+
+class arc(RelativePathCommand): # pylint: disable=invalid-name
+ """Relative Arc line segment"""
+
+ nargs = 7
+
+ @property
+ def args(self):
+ return self.rx, self.ry, self.x_axis_rotation, self.large_arc, self.sweep, self.dx, self.dy
+
+ def __init__(self, rx, ry, x_axis_rotation, large_arc, sweep, dx, dy):
+ self.rx = rx
+ self.ry = ry
+ self.x_axis_rotation = x_axis_rotation
+ self.large_arc = large_arc
+ self.sweep = sweep
+ self.dx = dx
+ self.dy = dy
+
+ def to_absolute(self, prev): # type: (Vector2d) -> "Arc"
+ x1, y1 = prev
+ return Arc(self.rx, self.ry, self.x_axis_rotation, self.large_arc, self.sweep, self.dx + x1, self.dy + y1)
+
+
+PathCommand._letter_to_class = {
+ "M": Move,
+ "L": Line,
+ "V": Vert,
+ "H": Horz,
+ "A": Arc,
+ "C": Curve,
+ "S": Smooth,
+ "Z": ZoneClose,
+ "Q": Quadratic,
+ "T": TepidQuadratic,
+ "m": move,
+ "l": line,
+ "v": vert,
+ "h": horz,
+ "a": arc,
+ "c": curve,
+ "s": smooth,
+ "z": zoneClose,
+ "q": quadratic,
+ "t": tepidQuadratic
+}
+
+
+class Path(list):
+ """A list of segment commands which combine to draw a shape"""
+
+ class PathCommandProxy(object):
+ """
+ A handy class for Path traverse and coordinate access
+
+ Reduces number of arguments in user code compared to bare :py:class:`PathCommand` methods
+ """
+
+ def __init__(self, command, first_point, previous_end_point, prev2_control_point):
+ self.command = command # type: PathCommand
+ self.first_point = first_point # type: Vector2d
+ self.previous_end_point = previous_end_point # type: Vector2d
+ self.prev2_control_point = prev2_control_point # type: Vector2d
+
+ @property
+ def name(self):
+ return self.command.name
+
+ @property
+ def letter(self):
+ return self.command.letter
+
+ @property
+ def next_command(self):
+ return self.command.next_command
+
+ @property
+ def is_relative(self):
+ return self.command.is_relative
+
+ @property
+ def is_absolute(self):
+ return self.command.is_absolute
+
+ @property
+ def args(self):
+ return self.command.args
+
+ @property
+ def control_points(self):
+ return self.command.control_points(self.first_point, self.previous_end_point, self.prev2_control_point)
+
+ @property
+ def end_point(self):
+ return self.command.end_point(self.first_point, self.previous_end_point)
+
+ def to_curve(self):
+ return self.command.to_curve(self.previous_end_point, self.prev2_control_point)
+
+ def to_curves(self):
+ return self.command.to_curves(self.previous_end_point, self.prev2_control_point)
+
+ def __str__(self):
+ return str(self.command)
+
+ def __repr__(self):
+ return "<" + self.__class__.__name__ + ">" + repr(self.command)
+
+ def __init__(self, path_d=None):
+ super(Path, self).__init__()
+ if isinstance(path_d, str):
+ # Returns a generator returning PathCommand objects
+ path_d = self.parse_string(path_d)
+ elif isinstance(path_d, CubicSuperPath):
+ path_d = path_d.to_path()
+
+ for item in (path_d or ()):
+ if isinstance(item, PathCommand):
+ self.append(item)
+ elif isinstance(item, (list, tuple)) and len(item) == 2:
+ if isinstance(item[1], (list, tuple)):
+ self.append(PathCommand.letter_to_class(item[0])(*item[1]))
+ else:
+ self.append(Line(*item))
+ else:
+ raise TypeError("Bad path type: {}({}, ...): {}".format(
+ type(path_d).__name__, type(item).__name__, item))
+
+ @classmethod
+ def parse_string(cls, path_d):
+ """Parse a path string and generate segment objects"""
+ for cmd, numbers in LEX_REX.findall(path_d):
+ args = list(strargs(numbers))
+ cmd = PathCommand.letter_to_class(cmd)
+ i = 0
+ while i < len(args) or cmd.nargs == 0:
+ seg = cmd(*args[i:i + cmd.nargs])
+ i += cmd.nargs
+ cmd = seg.next_command
+ yield seg
+
+ def bounding_box(self):
+ # type: () -> Optional[BoundingBox]
+ """Return bounding box of the Path"""
+ if not self:
+ return None
+ iterator = self.proxy_iterator()
+ proxy = next(iterator)
+ bbox = BoundingBox(proxy.first_point.x, proxy.first_point.y)
+ try:
+ while True:
+ proxy = next(iterator)
+ proxy.command.update_bounding_box(proxy.first_point, [
+ proxy.prev2_control_point,
+ proxy.previous_end_point,
+ ], bbox)
+ except StopIteration:
+ return bbox
+
+ def append(self, cmd):
+ """Append a command to this path including any chained commands"""
+ if isinstance(cmd, list):
+ self.extend(cmd)
+ elif isinstance(cmd, PathCommand):
+ super(Path, self).append(cmd)
+
+ def translate(self, x, y, inplace=False): # pylint: disable=invalid-name
+ """Move all coords in this path by the given amount"""
+ return self.transform(Transform(translate=(x, y)), inplace=inplace)
+
+ def scale(self, x, y, inplace=False): # pylint: disable=invalid-name
+ """Scale all coords in this path by the given amounts"""
+ return self.transform(Transform(scale=(x, y)), inplace=inplace)
+
+ def rotate(self, deg, center=None, inplace=False):
+ """Rotate the path around the given point"""
+ if center is None:
+ # Default center is center of bbox
+ bbox = self.bounding_box()
+ if bbox:
+ center = bbox.center
+ else:
+ center = Vector2d()
+ center = Vector2d(center)
+ return self.transform(Transform(rotate=(deg, center.x, center.y)), inplace=inplace)
+
+ @property
+ def control_points(self):
+
+ prev = Vector2d()
+ prev_prev = Vector2d()
+ first = Vector2d()
+
+ for i, seg in enumerate(self): # type: PathCommand
+ if i == 0:
+ first = seg.end_point(first, prev)
+ for cp in seg.control_points(first, prev, prev_prev):
+ prev_prev = prev
+ prev = cp
+ yield cp
+
+ @property
+ def end_points(self):
+ prev = Vector2d()
+ first = Vector2d()
+
+ for i, seg in enumerate(self): # type: PathCommand
+ if i == 0:
+ first = seg.end_point(first, prev)
+ end_point = seg.end_point(first, prev)
+ prev = end_point
+ yield end_point
+
+ def transform(self, transform, inplace=False):
+ """Convert to new path"""
+ result = Path()
+ previous = Vector2d()
+ previous_new = Vector2d()
+ first = Vector2d()
+ first_new = Vector2d()
+
+ for i, seg in enumerate(self): # type: PathCommand
+ if i == 0:
+ first = seg.end_point(first, previous)
+
+ if isinstance(seg, (horz, Horz, Vert, vert)):
+ seg = seg.to_line(previous)
+
+ if seg.is_relative:
+ new_seg = seg.to_absolute(previous).transform(transform).to_relative(previous_new)
+ else:
+ new_seg = seg.transform(transform)
+
+ if i == 0:
+ first_new = new_seg.end_point(first_new, previous_new)
+
+ if inplace:
+ self[i] = new_seg
+ else:
+ result.append(new_seg)
+ previous = seg.end_point(first, previous)
+ previous_new = new_seg.end_point(first_new, previous_new)
+ if inplace:
+ return self
+ return result
+
+ def reverse(self):
+ """Returns a reversed path"""
+ pass
+
+ def close(self):
+ """Attempt to close the last path segment"""
+ if self and not isinstance(self[-1], (zoneClose, ZoneClose)):
+ self.append(ZoneClose())
+
+ def proxy_iterator(self):
+ """
+ Yields :py:class:`AugmentedPathIterator`
+
+ :rtype: Iterator[ Path.PathCommandProxy ]
+ """
+
+ previous = Vector2d()
+ prev_prev = Vector2d()
+ first = Vector2d()
+
+ for i, seg in enumerate(self): # type: PathCommand
+ if i == 0:
+ prev_prev = previous = first = seg.end_point(first, previous)
+ yield Path.PathCommandProxy(seg, first, previous, prev_prev)
+ if isinstance(seg, (curve, tepidQuadratic, quadratic, smooth,
+ Curve, TepidQuadratic, Quadratic, Smooth)):
+ prev_prev = list(seg.control_points(first, previous, prev_prev))[-2]
+ previous = seg.end_point(first, previous)
+
+ def to_absolute(self):
+ """Convert this path to use only absolute coordinates"""
+ abspath = Path()
+
+ previous = Vector2d()
+ first = Vector2d()
+
+ for seg in self: # type: PathCommand
+ if isinstance(seg, (move, Move)):
+ first = seg.end_point(first, previous)
+
+ abspath.append(seg.to_absolute(previous))
+ previous = seg.end_point(first, previous)
+
+ return abspath
+
+ def to_relative(self):
+ """Convert this path to use only relative coordinates"""
+ abspath = Path()
+
+ previous = Vector2d()
+ first = Vector2d()
+
+ for seg in self: # type: PathCommand
+ if isinstance(seg, (move, Move)):
+ first = seg.end_point(first, previous)
+
+ abspath.append(seg.to_relative(previous))
+ previous = seg.end_point(first, previous)
+
+ return abspath
+
+ def __str__(self):
+ return " ".join([str(seg) for seg in self])
+
+ def __add__(self, other):
+ acopy = copy.deepcopy(self)
+ if isinstance(other, str):
+ other = Path(other)
+ if isinstance(other, list):
+ acopy.extend(other)
+ return acopy
+
+ def to_arrays(self):
+ """Returns path in format of parsePath output, returning arrays of absolute command data
+
+ .. deprecated:: 1.0
+ This is compatibility function for older API. Should not be used in new code
+
+ """
+ return [[seg.letter, list(seg.args)] for seg in self.to_absolute()]
+
+ def to_superpath(self):
+ """Convert this path into a cubic super path"""
+ return CubicSuperPath(self)
+
+ def copy(self):
+ """Make a copy"""
+ return copy.deepcopy(self)
+
+
+class CubicSuperPath(list):
+ """
+ A conversion of a path into a predictable list of cubic curves which
+ can be operated on as a list of simplified instructions.
+
+ When converting back into a path, all lines, arcs etc will be converted
+ to curve instructions.
+
+ Structure is held as [SubPath[(point_a, bezier, point_b), ...]], ...]
+ """
+
+ def __init__(self, items):
+ super(CubicSuperPath, self).__init__()
+ self._closed = True
+ self._prev = Vector2d()
+ self._prev_prev = Vector2d()
+
+
+ if isinstance(items, str):
+ items = Path(items)
+
+ if isinstance(items, Path):
+ items = items.to_absolute()
+
+ for item in items:
+ self.append(item)
+
+ def __str__(self):
+ return str(self.to_path())
+
+ def append(self, item):
+ """Accept multiple different formats for the data"""
+ if isinstance(item, list) and len(item) == 2 and isinstance(item[0], str):
+ item = PathCommand.letter_to_class(item[0])(*item[1])
+ is_quadratic = False
+ if isinstance(item, PathCommand):
+ if isinstance(item, Move):
+ if self._closed is False:
+ super(CubicSuperPath, self).append([])
+ item = [list(item.args), list(item.args), list(item.args)]
+ elif isinstance(item, ZoneClose) and self and self[-1]:
+ # This duplicates the first segment to 'close' the path, it's appended directly
+ # because we don't want to last coord to change for the final segment.
+ self[-1].append([self[-1][0][0][:], self[-1][0][1][:], self[-1][0][2][:]])
+ # Then adds a new subpath for the next shape (if any)
+ self._closed = True
+ self._prev.assign(self._first)
+ return
+ elif isinstance(item, Arc):
+ # Arcs are made up of three curves (approximated)
+ for arc_curve in item.to_curves(self._prev, self._prev_prev):
+ x2, y2, x3, y3, x4, y4 = arc_curve.args
+ self.append([[x2, y2], [x3, y3], [x4, y4]])
+ self._prev_prev.assign(x3, y3)
+ return
+ else:
+ is_quadratic = isinstance(item, (Quadratic, TepidQuadratic, quadratic, tepidQuadratic))
+ if isinstance(item, (Horz, Vert)):
+ item = item.to_line(self._prev)
+ pp = self._prev_prev
+ if is_quadratic:
+ self._prev_prev = list(item.control_points(self._first, self._prev, pp))[-2:-1][0]
+ item = item.to_curve(self._prev, pp)
+
+ if isinstance(item, Curve):
+ # Curves are cut into three tuples for the super path.
+ item = item.to_bez()
+
+ if not isinstance(item, list):
+ raise ValueError("Unknown super curve item type: {}".format(item))
+
+ if len(item) != 3 or not all([len(bit) == 2 for bit in item]):
+ # The item is already a subpath (usually from some other process)
+ if len(item[0]) == 3 and all([len(bit) == 2 for bit in item[0]]):
+ super(CubicSuperPath, self).append(self._clean(item))
+ self._prev_prev = Vector2d(self[-1][-1][0])
+ self._prev = Vector2d(self[-1][-1][1])
+ return
+ raise ValueError("Unknown super curve list format: {}".format(item))
+
+ if self._closed:
+ # Closed means that the previous segment is closed so we need a new one
+ # We always append to the last open segment. CSP starts out closed.
+ self._closed = False
+ super(CubicSuperPath, self).append([])
+
+ if self[-1]:
+ # The last tuple is replaced, it's the coords of where the next segment will land.
+ self[-1][-1][-1] = item[0][:]
+ # The last coord is duplicated, but is expected to be replaced
+ self[-1].append(item[1:] + copy.deepcopy(item)[-1:])
+
+ self._prev = Vector2d(self[-1][-1][1])
+ if not is_quadratic:
+ self._prev_prev = Vector2d(self[-1][-1][0])
+
+ def _clean(self, lst):
+ """Recursively clean lists so they have the same type"""
+ if isinstance(lst, (tuple, list)):
+ return [self._clean(child) for child in lst]
+ return lst
+
+ @property
+ def _first(self):
+ try:
+ return Vector2d(self[-1][0][0])
+ except IndexError:
+ return Vector2d()
+
+ def to_path(self, curves_only=False):
+ """Convert the super path back to an svg path"""
+ return Path(list(self.to_segments(curves_only)))
+
+ def to_segments(self, curves_only=False):
+ """Generate a set of segments for this cubic super path"""
+ for subpath in self:
+ previous = []
+ for segment in subpath:
+ if not previous:
+ yield Move(*segment[1][:])
+ elif self.is_line(previous, segment) and not curves_only:
+ if segment is subpath[-1] and Vector2d(segment[1]).is_close(subpath[0][1]):
+ yield ZoneClose()
+ else:
+ yield Line(*segment[1][:])
+ else:
+ yield Curve(*(previous[2][:] + segment[0][:] + segment[1][:]))
+ previous = segment
+
+ def transform(self, transform):
+ """Apply a transformation matrix to this super path"""
+ return self.to_path().transform(transform).to_superpath()
+
+ @staticmethod
+ def is_line(previous, segment):
+ """Check whether csp segment (two points) has retracted handles."""
+ return Vector2d(previous[1]).is_close(previous[2]) and \
+ Vector2d(segment[0]).is_close(segment[1])
+
+def arc_to_path(point, params):
+ """Approximates an arc with cubic bezier segments.
+
+ Arguments:
+ point: Starting point (absolute coords)
+ params: Arcs parameters as per
+ https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
+
+ Returns a list of triplets of points : [control_point_before, node, control_point_after]
+ (first and last returned triplets are [p1, p1, *] and [*, p2, p2])
+ """
+ A = point[:]
+ rx, ry, teta, longflag, sweepflag, x2, y2 = params[:]
+ teta = teta * pi / 180.0
+ B = [x2, y2]
+ # Degenerate ellipse
+ if rx == 0 or ry == 0 or A == B:
+ return [[A[:], A[:], A[:]], [B[:], B[:], B[:]]]
+
+ # turn coordinates so that the ellipse morph into a *unit circle* (not 0-centered)
+ mat = matprod((rotmat(teta), [[1.0 / rx, 0.0], [0.0, 1.0 / ry]], rotmat(-teta)))
+ applymat(mat, A)
+ applymat(mat, B)
+
+ k = [-(B[1] - A[1]), B[0] - A[0]]
+ d = k[0] * k[0] + k[1] * k[1]
+ k[0] /= sqrt(d)
+ k[1] /= sqrt(d)
+ d = sqrt(max(0, 1 - d / 4.0))
+ # k is the unit normal to AB vector, pointing to center O
+ # d is distance from center to AB segment (distance from O to the midpoint of AB)
+ # for the last line, remember this is a unit circle, and kd vector is ortogonal to AB (Pythagorean thm)
+
+ if longflag == sweepflag: # top-right ellipse in SVG example https://www.w3.org/TR/SVG/images/paths/arcs02.svg
+ d *= -1
+
+ O = [(B[0] + A[0]) / 2.0 + d * k[0], (B[1] + A[1]) / 2.0 + d * k[1]]
+ OA = [A[0] - O[0], A[1] - O[1]]
+ OB = [B[0] - O[0], B[1] - O[1]]
+ start = acos(OA[0] / norm(OA))
+ if OA[1] < 0:
+ start *= -1
+ end = acos(OB[0] / norm(OB))
+ if OB[1] < 0:
+ end *= -1
+ # start and end are the angles from center of the circle to A and to B respectively
+
+ if sweepflag and start > end:
+ end += 2 * pi
+ if (not sweepflag) and start < end:
+ end -= 2 * pi
+
+ NbSectors = int(abs(start - end) * 2 / pi) + 1
+ dTeta = (end - start) / NbSectors
+ v = 4 * tan(dTeta / 4.) / 3.
+ # I would use v = tan(dTeta/2)*4*(sqrt(2)-1)/3 ?
+ p = []
+ for i in range(0, NbSectors + 1, 1):
+ angle = start + i * dTeta
+ v1 = [O[0] + cos(angle) - (-v) * sin(angle), O[1] + sin(angle) + (-v) * cos(angle)]
+ pt = [O[0] + cos(angle), O[1] + sin(angle)]
+ v2 = [O[0] + cos(angle) - v * sin(angle), O[1] + sin(angle) + v * cos(angle)]
+ p.append([v1, pt, v2])
+ p[0][0] = p[0][1][:]
+ p[-1][2] = p[-1][1][:]
+
+ # go back to the original coordinate system
+ mat = matprod((rotmat(teta), [[rx, 0], [0, ry]], rotmat(-teta)))
+ for pts in p:
+ applymat(mat, pts[0])
+ applymat(mat, pts[1])
+ applymat(mat, pts[2])
+ return p
+
+
+def matprod(mlist):
+ """Get the product of the mat"""
+ prod = mlist[0]
+ for mat in mlist[1:]:
+ a00 = prod[0][0] * mat[0][0] + prod[0][1] * mat[1][0]
+ a01 = prod[0][0] * mat[0][1] + prod[0][1] * mat[1][1]
+ a10 = prod[1][0] * mat[0][0] + prod[1][1] * mat[1][0]
+ a11 = prod[1][0] * mat[0][1] + prod[1][1] * mat[1][1]
+ prod = [[a00, a01], [a10, a11]]
+ return prod
+
+
+def rotmat(teta):
+ """Rotate the mat"""
+ return [[cos(teta), -sin(teta)], [sin(teta), cos(teta)]]
+
+
+def applymat(mat, point):
+ """Apply the given mat"""
+ x = mat[0][0] * point[0] + mat[0][1] * point[1]
+ y = mat[1][0] * point[0] + mat[1][1] * point[1]
+ point[0] = x
+ point[1] = y
+
+
+def norm(point):
+ """Normalise"""
+ return sqrt(point[0] * point[0] + point[1] * point[1])