diff options
Diffstat (limited to 'share/extensions/inkex/elements/_polygons.py')
-rw-r--r-- | share/extensions/inkex/elements/_polygons.py | 231 |
1 files changed, 231 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..0bcbc38 --- /dev/null +++ b/share/extensions/inkex/elements/_polygons.py @@ -0,0 +1,231 @@ +# -*- 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 ..paths import Path +from ..transforms import Transform, ImmutableVector2d, Vector2d +from ..utils import addNS +from ..units import convert_unit + +from ._base import ShapeElement + +class PathElementBase(ShapeElement): + """Base element for path based shapes""" + get_path = lambda self: self.get('d') + + @classmethod + def new(cls, path, **attrs): + return super(PathElementBase, cls).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' + + @classmethod + def arc(cls, center, rx, ry=None, **kw): # pylint: disable=invalid-name + """Generate a sodipodi arc (special type)""" + 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') + for name, value in others: + if value is not None: + elem.set('sodipodi:'+name, str(value).lower()) + return elem + + @classmethod + def star(cls, center, radi, sides, rounded=None): + """Generate a sodipodi start (special type)""" + elem = cls() + elem.set('sodipodi:cx', center[0]) + elem.set('sodipodi:cy', center[1]) + elem.set('sodipodi:r1', radi[0]) + elem.set('sodipodi:r2', radi[1]) + elem.set('sodipodi:arg1', 0.85) + elem.set('sodipodi:arg2', 1.3) + elem.set('sodipodi:sides', sides) + elem.set('inkscape:rounded', rounded) + elem.set('sodipodi:type', 'star') + 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 = ['{:g},{:g}'.format(x, y) 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: 'M' + self.get('points') + ' Z' + + +class Line(ShapeElement): + """A line segment connecting two points""" + tag_name = 'line' + get_path = lambda self: 'M{0[x1]},{0[y1]} L{0[x2]},{0[y2]} Z'.format(self.attrib) + + @classmethod + def new(cls, start, end, **attrs): + start = Vector2d(start) + end = Vector2d(end) + return super(Line, cls).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: convert_unit(self.get('x', '0'), 'px')) + top = property(lambda self: convert_unit(self.get('y', '0'), 'px')) + right = property(lambda self: self.left + self.width) + bottom = property(lambda self: self.top + self.height) + width = property(lambda self: convert_unit(self.get('width', '0'), 'px')) + height = property(lambda self: convert_unit(self.get('height', '0'), 'px')) + rx = property(lambda self: convert_unit(self.get('rx', self.get('ry', 0.0)), 'px')) + ry = property(lambda self: convert_unit(self.get('ry', self.get('rx', 0.0)), 'px')) # 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 + return 'M {1},{0.top}'\ + 'L {2},{0.top} A {0.rx},{0.ry} 0 0 1 {0.right},{3}'\ + 'L {0.right},{4} A {0.rx},{0.ry} 0 0 1 {2},{0.bottom}'\ + 'L {1},{0.bottom} A {0.rx},{0.ry} 0 0 1 {0.left},{4}'\ + 'L {0.left},{3} A {0.rx},{0.ry} 0 0 1 {1},{0.top} z'\ + .format(self, self.left + rx, self.right - rx, self.top + ry, self.bottom - ry) + + return 'M {0.left},{0.top} h{0.width}v{0.height}h{1} z'.format(self, -self.width) + + +class Rectangle(RectangleBase): + """Provide a useful extension for rectangle elements""" + tag_name = 'rect' + + @classmethod + def new(cls, left, top, width, height, **attrs): + return super(Rectangle, cls).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 ImmutableVector2d(convert_unit(self.get('cx', '0'), 'px'), convert_unit(self.get('cy', '0'), 'px')) + + @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(EllipseBase, cls).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): + return convert_unit(self.get('r', '0'), 'px') + + @radius.setter + def radius(self, value): + self.set("r", value) + + def _rxry(self): + r = self.radius + return Vector2d(r, r) + + +class Ellipse(EllipseBase): + """Provide a similar extension to the Circle interface""" + tag_name = 'ellipse' + + @property + def radius(self): + return ImmutableVector2d(convert_unit(self.get('rx', '0'), 'px'), convert_unit(self.get('ry', '0'), 'px')) + + @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 |