summaryrefslogtreecommitdiffstats
path: root/share/extensions/inkex/elements/_polygons.py
diff options
context:
space:
mode:
Diffstat (limited to 'share/extensions/inkex/elements/_polygons.py')
-rw-r--r--share/extensions/inkex/elements/_polygons.py507
1 files changed, 507 insertions, 0 deletions
diff --git a/share/extensions/inkex/elements/_polygons.py b/share/extensions/inkex/elements/_polygons.py
new file mode 100644
index 0000000..4f88a3a
--- /dev/null
+++ b/share/extensions/inkex/elements/_polygons.py
@@ -0,0 +1,507 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020 Martin Owens <doctormo@gmail.com>
+# Sergei Izmailov <sergei.a.izmailov@gmail.com>
+# Thomas Holder <thomas.holder@schrodinger.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.
+#
+# pylint: disable=arguments-differ
+"""
+Interface for all shapes/polygons such as lines, paths, rectangles, circles etc.
+"""
+
+from math import cos, pi, sin
+from typing import Optional, Tuple
+from ..paths import Arc, Curve, Move, Path, ZoneClose
+from ..paths import Line as PathLine
+from ..transforms import Transform, ImmutableVector2d, Vector2d
+from ..bezier import pointdistance
+
+from ._utils import addNS
+from ._base import ShapeElement
+
+
+class PathElementBase(ShapeElement):
+ """Base element for path based shapes"""
+
+ get_path = lambda self: Path(self.get("d"))
+
+ @classmethod
+ def new(cls, path, **attrs):
+ return super().new(d=Path(path), **attrs)
+
+ def set_path(self, path):
+ """Set the given data as a path as the 'd' attribute"""
+ self.set("d", str(Path(path)))
+
+ def apply_transform(self):
+ """Apply the internal transformation to this node and delete"""
+ if "transform" in self.attrib:
+ self.path = self.path.transform(self.transform)
+ self.set("transform", Transform())
+
+ @property
+ def original_path(self):
+ """Returns the original path if this is a LPE, or the path if not"""
+ return Path(self.get("inkscape:original-d", self.path))
+
+ @original_path.setter
+ def original_path(self, path):
+ if addNS("inkscape:original-d") in self.attrib:
+ self.set("inkscape:original-d", str(Path(path)))
+ else:
+ self.path = path
+
+
+class PathElement(PathElementBase):
+ """Provide a useful extension for path elements"""
+
+ tag_name = "path"
+
+ @staticmethod
+ def _arcpath(
+ cx: float,
+ cy: float,
+ rx: float,
+ ry: float,
+ start: float,
+ end: float,
+ arctype: str,
+ ) -> Optional[Path]:
+ """Compute the path for an arc defined by Inkscape-specific attributes.
+
+ For details on arguments, see :func:`arc`.
+
+ .. versionadded:: 1.2"""
+ if abs(rx) < 1e-8 or abs(ry) < 1e-8:
+ return None
+ incr = end - start
+ if incr < 0:
+ incr += 2 * pi
+ numsegs = min(1 + int(incr * 2.0 / pi), 4)
+ incr = incr / numsegs
+
+ computed = Path()
+ computed.append(Move(cos(start), sin(start)))
+ for seg in range(1, numsegs + 1):
+ computed.append(
+ Arc(1, 1, 0, 0, 1, cos(start + seg * incr), sin(start + seg * incr))
+ )
+ if abs(incr * numsegs - 2 * pi) > 1e-8 and (
+ arctype in ("slice", "")
+ ): # slice is default
+ computed.append(PathLine(0, 0))
+ if arctype != "arc":
+ computed.append(ZoneClose())
+ computed.transform(
+ Transform().add_translate(cx, cy).add_scale(rx, ry), inplace=True
+ )
+ return computed.to_relative()
+
+ @classmethod
+ def arc(
+ cls, center, rx, ry=None, arctype="", pathonly=False, **kw
+ ): # pylint: disable=invalid-name
+ """Generates a sodipodi elliptical arc (special type). Also computes the path
+ that Inkscape uses under the hood.
+ All data may be given as parseable strings or using numeric data types.
+
+ Args:
+ center (tuple-like): Coordinates of the star/polygon center as tuple or
+ Vector2d
+ rx (Union[float, str]): Radius in x direction
+ ry (Union[float, str], optional): Radius in y direction. If not given,
+ ry=rx. Defaults to None.
+ arctype (str, optional): "arc", "chord" or "slice". Defaults to "", i.e.
+ "slice".
+
+ .. versionadded:: 1.2
+ Previously set to "arc" as fixed value
+ pathonly (bool, optional): Whether to create the path without
+ Inkscape-specific attributes. Defaults to False.
+
+ .. versionadded:: 1.2
+ Keyword args:
+ start (Union[float, str]): start angle in radians
+ end (Union[float, str]): end angle in radians
+ open (str): whether the path should be open (true/false). Not used in
+ Inkscape > 1.1
+
+ Returns:
+ PathElement : the created star/polygon
+ """
+ others = [(name, kw.pop(name, None)) for name in ("start", "end", "open")]
+ elem = cls(**kw)
+ elem.set("sodipodi:cx", center[0])
+ elem.set("sodipodi:cy", center[1])
+ elem.set("sodipodi:rx", rx)
+ elem.set("sodipodi:ry", ry or rx)
+ elem.set("sodipodi:type", "arc")
+ if arctype != "":
+ elem.set("sodipodi:arc-type", arctype)
+ for name, value in others:
+ if value is not None:
+ elem.set("sodipodi:" + name, str(value).lower())
+
+ path = cls._arcpath(
+ float(center[0]),
+ float(center[1]),
+ float(rx),
+ float(ry or rx),
+ float(elem.get("sodipodi:start", 0)),
+ float(elem.get("sodipodi:end", 2 * pi)),
+ arctype,
+ )
+ if pathonly:
+ elem = cls(**kw)
+ if path is not None:
+ elem.path = path
+ return elem
+
+ @staticmethod
+ def _starpath(
+ c: Tuple[float, float],
+ sides: int,
+ r: Tuple[float, float], # pylint: disable=invalid-name
+ arg: Tuple[float, float],
+ rounded: float,
+ flatsided: bool,
+ ):
+ """Helper method to generate the path for an Inkscape star/ polygon; randomized
+ is ignored.
+
+ For details on arguments, see :func:`star`.
+
+ .. versionadded:: 1.2"""
+
+ def _star_get_xy(point, index):
+ cur_arg = arg[point] + 2 * pi / sides * (index % sides)
+ return Vector2d(*c) + r[point] * Vector2d(cos(cur_arg), sin(cur_arg))
+
+ def _rot90_rel(origin, other):
+ """Returns a unit length vector at 90 deg from origin to other"""
+ return (
+ 1
+ / pointdistance(other, origin)
+ * Vector2d(other.y - origin.y, other.x - origin.x)
+ )
+
+ def _star_get_curvepoint(point, index, is_prev: bool):
+ index = index % sides
+ orig = _star_get_xy(point, index)
+ previ = (index - 1 + sides) % sides
+ nexti = (index + 1) % sides
+ # neighbors of the current point depend on polygon or star
+ prev = (
+ _star_get_xy(point, previ)
+ if flatsided
+ else _star_get_xy(1 - point, index if point == 1 else previ)
+ )
+ nextp = (
+ _star_get_xy(point, nexti)
+ if flatsided
+ else _star_get_xy(1 - point, index if point == 0 else nexti)
+ )
+ mid = 0.5 * (prev + nextp)
+ # direction of bezier handles
+ rot = _rot90_rel(orig, mid + 100000 * _rot90_rel(mid, nextp))
+ ret = (
+ rounded
+ * rot
+ * (
+ -1 * pointdistance(prev, orig)
+ if is_prev
+ else pointdistance(nextp, orig)
+ )
+ )
+ return orig + ret
+
+ pointy = abs(rounded) < 1e-4
+ result = Path()
+ result.append(Move(*_star_get_xy(0, 0)))
+ for i in range(0, sides):
+ # draw to point type 1 for stars
+ if not flatsided:
+ if pointy:
+ result.append(PathLine(*_star_get_xy(1, i)))
+ else:
+ result.append(
+ Curve(
+ *_star_get_curvepoint(0, i, False),
+ *_star_get_curvepoint(1, i, True),
+ *_star_get_xy(1, i),
+ )
+ )
+ # draw to point type 0 for both stars and rectangles
+ if pointy and i < sides - 1:
+ result.append(PathLine(*_star_get_xy(0, i + 1)))
+ if not pointy:
+ if not flatsided:
+ result.append(
+ Curve(
+ *_star_get_curvepoint(1, i, False),
+ *_star_get_curvepoint(0, i + 1, True),
+ *_star_get_xy(0, i + 1),
+ )
+ )
+ else:
+ result.append(
+ Curve(
+ *_star_get_curvepoint(0, i, False),
+ *_star_get_curvepoint(0, i + 1, True),
+ *_star_get_xy(0, i + 1),
+ )
+ )
+
+ result.append(ZoneClose())
+ return result.to_relative()
+
+ @classmethod
+ def star(
+ cls,
+ center,
+ radii,
+ sides=5,
+ rounded=0,
+ args=(0, 0),
+ flatsided=False,
+ pathonly=False,
+ ):
+ """Generate a sodipodi star / polygon. Also computes the path that Inkscape uses
+ under the hood. The arguments for center, radii, sides, rounded and args can be
+ given as strings or as numeric data.
+
+ .. versionadded:: 1.1
+
+ Args:
+ center (Tuple-like): Coordinates of the star/polygon center as tuple or
+ Vector2d
+ radii (tuple): Radii of the control points, i.e. their distances from the
+ center. The control points are specified in polar coordinates. Only the
+ first control point is used for polygons.
+ sides (int, optional): Number of sides / tips of the polygon / star.
+ Defaults to 5.
+ rounded (int, optional): Controls the rounding radius of the polygon / star.
+ For `rounded=0`, only straight lines are used. Defaults to 0.
+ args (tuple, optional): Angle between horizontal axis and control points.
+ Defaults to (0,0).
+
+ .. versionadded:: 1.2
+ Previously fixed to (0.85, 1.3)
+ flatsided (bool, optional): True for polygons, False for stars.
+ Defaults to False.
+
+ .. versionadded:: 1.2
+ pathonly (bool, optional): Whether to create the path without
+ Inkscape-specific attributes. Defaults to False.
+
+ .. versionadded:: 1.2
+
+ Returns:
+ PathElement : the created star/polygon
+ """
+ elem = cls()
+ elem.set("sodipodi:cx", center[0])
+ elem.set("sodipodi:cy", center[1])
+ elem.set("sodipodi:r1", radii[0])
+ elem.set("sodipodi:r2", radii[1])
+ elem.set("sodipodi:arg1", args[0])
+ elem.set("sodipodi:arg2", args[1])
+ elem.set("sodipodi:sides", max(sides, 3) if flatsided else max(sides, 2))
+ elem.set("inkscape:rounded", rounded)
+ elem.set("inkscape:flatsided", str(flatsided).lower())
+ elem.set("sodipodi:type", "star")
+
+ path = cls._starpath(
+ (float(center[0]), float(center[1])),
+ int(sides),
+ (float(radii[0]), float(radii[1])),
+ (float(args[0]), float(args[1])),
+ float(rounded),
+ flatsided,
+ )
+ if pathonly:
+ elem = cls()
+ # inkex.errormsg(path)
+ if path is not None:
+ elem.path = path
+
+ return elem
+
+
+class Polyline(ShapeElement):
+ """Like a path, but made up of straight line segments only"""
+
+ tag_name = "polyline"
+
+ def get_path(self):
+ return Path("M" + self.get("points"))
+
+ def set_path(self, path):
+ points = [f"{x:g},{y:g}" for x, y in Path(path).end_points]
+ self.set("points", " ".join(points))
+
+
+class Polygon(ShapeElement):
+ """A closed polyline"""
+
+ tag_name = "polygon"
+ get_path = lambda self: Path("M" + self.get("points") + " Z")
+
+
+class Line(ShapeElement):
+ """A line segment connecting two points"""
+
+ tag_name = "line"
+ x1 = property(lambda self: self.to_dimensionless(self.get("x1", 0)))
+ y1 = property(lambda self: self.to_dimensionless(self.get("y1", 0)))
+ x2 = property(lambda self: self.to_dimensionless(self.get("x2", 0)))
+ y2 = property(lambda self: self.to_dimensionless(self.get("y2", 0)))
+ get_path = lambda self: Path(f"M{self.x1},{self.y1} L{self.x2},{self.y2}")
+
+ @classmethod
+ def new(cls, start, end, **attrs):
+ start = Vector2d(start)
+ end = Vector2d(end)
+ return super().new(x1=start.x, y1=start.y, x2=end.x, y2=end.y, **attrs)
+
+
+class RectangleBase(ShapeElement):
+ """Provide a useful extension for rectangle elements"""
+
+ left = property(lambda self: self.to_dimensionless(self.get("x", "0")))
+ top = property(lambda self: self.to_dimensionless(self.get("y", "0")))
+ right = property(lambda self: self.left + self.width)
+ bottom = property(lambda self: self.top + self.height)
+ width = property(lambda self: self.to_dimensionless(self.get("width", "0")))
+ height = property(lambda self: self.to_dimensionless(self.get("height", "0")))
+ rx = property(
+ lambda self: self.to_dimensionless(self.get("rx", self.get("ry", 0.0)))
+ )
+ ry = property(
+ lambda self: self.to_dimensionless(self.get("ry", self.get("rx", 0.0)))
+ ) # pylint: disable=invalid-name
+
+ def get_path(self):
+ """Calculate the path as the box around the rect"""
+ if self.rx:
+ rx, ry = self.rx, self.ry # pylint: disable=invalid-name
+ cpts = [self.left + rx, self.right - rx, self.top + ry, self.bottom - ry]
+ return (
+ f"M {cpts[0]},{self.top}"
+ f"L {cpts[1]},{self.top} "
+ f"A {self.rx},{self.ry} 0 0 1 {self.right},{cpts[2]}"
+ f"L {self.right},{cpts[3]} "
+ f"A {self.rx},{self.ry} 0 0 1 {cpts[1]},{self.bottom}"
+ f"L {cpts[0]},{self.bottom} "
+ f"A {self.rx},{self.ry} 0 0 1 {self.left},{cpts[3]}"
+ f"L {self.left},{cpts[2]} "
+ f"A {self.rx},{self.ry} 0 0 1 {cpts[0]},{self.top} z"
+ )
+
+ return f"M {self.left},{self.top} h{self.width}v{self.height}h{-self.width} z"
+
+
+class Rectangle(RectangleBase):
+ """Provide a useful extension for rectangle elements"""
+
+ tag_name = "rect"
+
+ @classmethod
+ def new(cls, left, top, width, height, **attrs):
+ return super().new(x=left, y=top, width=width, height=height, **attrs)
+
+
+class EllipseBase(ShapeElement):
+ """Absorbs common part of Circle and Ellipse classes"""
+
+ def get_path(self):
+ """Calculate the arc path of this circle"""
+ rx, ry = self._rxry()
+ cx, y = self.center.x, self.center.y - ry
+ return (
+ "M {cx},{y} "
+ "a {rx},{ry} 0 1 0 {rx}, {ry} "
+ "a {rx},{ry} 0 0 0 -{rx}, -{ry} z"
+ ).format(cx=cx, y=y, rx=rx, ry=ry)
+
+ @property
+ def center(self):
+ """Return center of circle/ellipse"""
+ return ImmutableVector2d(
+ self.to_dimensionless(self.get("cx", "0")),
+ self.to_dimensionless(self.get("cy", "0")),
+ )
+
+ @center.setter
+ def center(self, value):
+ value = Vector2d(value)
+ self.set("cx", value.x)
+ self.set("cy", value.y)
+
+ def _rxry(self):
+ # type: () -> Vector2d
+ """Helper function"""
+ raise NotImplementedError()
+
+ @classmethod
+ def new(cls, center, radius, **attrs):
+ circle = super().new(**attrs)
+ circle.center = center
+ circle.radius = radius
+ return circle
+
+
+class Circle(EllipseBase):
+ """Provide a useful extension for circle elements"""
+
+ tag_name = "circle"
+
+ @property
+ def radius(self) -> float:
+ """Return radius of circle"""
+ return self.to_dimensionless(self.get("r", "0"))
+
+ @radius.setter
+ def radius(self, value):
+ self.set("r", self.to_dimensionless(value))
+
+ def _rxry(self):
+ r = self.radius
+ return Vector2d(r, r)
+
+
+class Ellipse(EllipseBase):
+ """Provide a similar extension to the Circle interface for ellipses"""
+
+ tag_name = "ellipse"
+
+ @property
+ def radius(self) -> ImmutableVector2d:
+ """Return radii of ellipse"""
+ return ImmutableVector2d(
+ self.to_dimensionless(self.get("rx", "0")),
+ self.to_dimensionless(self.get("ry", "0")),
+ )
+
+ @radius.setter
+ def radius(self, value):
+ value = Vector2d(value)
+ self.set("rx", str(value.x))
+ self.set("ry", str(value.y))
+
+ def _rxry(self):
+ return self.radius