#!/usr/bin/env python3

# twist.py -- Primarily a simple example of writing an Inkscape extension
#             which manipulates objects in a drawing.
#
# For a polygon with vertices V[0], V[1], V[2], ..., V[n-1] iteratively
# move each vertex V[i] by a constant factor 0 < s < 1.0 along the edge
# between V[i] and V[i+1 modulo n] for 0 <= i <= n-1.
#
# This extension operates on every selected closed path, or, if no paths
# are selected, then every closed path in the document.  Since the "twisting"
# effect only concerns itself with individual paths, no effort is made to
# worry about the transforms applied to the paths.  That is, it is not
# necessary to worry about tracking SVG transforms as all the work can be
# done using the untransformed coordinates of each path.

# Written by Daniel C. Newman ( dan dot newman at mtbaldy dot us )
# 19 October 2010

# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

import inkex
from inkex import Transform
from inkex import bezier
from inkex.paths import Path, CubicSuperPath
from lxml import etree

def subdivideCubicPath(sp, flat, i=1):
    """
    [ Lifted from eggbot.py with impunity ]

    Break up a bezier curve into smaller curves, each of which
    is approximately a straight line within a given tolerance
    (the "smoothness" defined by [flat]).

    This is a modified version of cspsubdiv.cspsubdiv(): rewritten
    because recursion-depth errors on complicated line segments
    could occur with cspsubdiv.cspsubdiv().
    """

    while True:
        while True:
            if i >= len(sp):
                return

            p0 = sp[i - 1][1]
            p1 = sp[i - 1][2]
            p2 = sp[i][0]
            p3 = sp[i][1]

            b = (p0, p1, p2, p3)

            if bezier.maxdist(b) > flat:
                break

            i += 1

        one, two = bezier.beziersplitatt(b, 0.5)
        sp[i - 1][2] = one[1]
        sp[i][0] = two[2]
        p = [one[2], one[3], two[1]]
        sp[i:1] = [p]


def distanceSquared(p1, p2):
    """
    Pythagorean distance formula WITHOUT the square root.  Since
    we just want to know if the distance is less than some fixed
    fudge factor, we can just square the fudge factor once and run
    with it rather than compute square roots over and over.
    """

    dx = p2[0] - p1[0]
    dy = p2[1] - p1[1]

    return dx * dx + dy * dy


class Twist(inkex.EffectExtension):

    def add_arguments(self, pars):
        pars.add_argument("--nSteps", type=int,  default=8, help="Number of iterations to take")
        pars.add_argument("--fRatio", type=float, default=0.2, help="Some ratio")

        """
        Store each path in an associative array (dictionary) indexed
        by the lxml.etree pointer for the SVG document element
        containing the path.  Looking up the path in the dictionary
        yields a list of lists.  Each of these lists is a subpath
        # of the path.  E.g., for the SVG path

            <path d="M 10,10 l 0,5 l 5,0 l 0,-5 Z M 30,30 L 30,60"/>

         we'd have two subpaths which will be reduced to absolute
         coordinates.

            subpath_1 = [ [10, 10], [10, 15], [15, 15], [15, 10], [10,10] ]
            subpath_2 = [ [30, 30], [30, 60] ]
            self.paths[<node pointer>] = [ subpath_1, subpath_2 ]

        All of the paths and their subpaths could be drawn as follows:

            for path in self.paths:
                for subpath in self.paths[path]:
                    first = True
                    for vertex in subpath:
                        if first:
                            moveto( vertex[0], vertex[1] )
                            first = False
                        else:
                            lineto( vertex[0], vertex[1] )

        NOTE: drawing all the paths like the above would not in general
        give the correct rendering of the document UNLESS path transforms
        were also tracked and applied.
        """

        self.paths = {}
        self.paths_clone_transform = {}

    def addPathVertices(self, path, node=None, transform=None, clone_transform=None):

        """
        Decompose the path data from an SVG element into individual
        subpaths, each subpath consisting of absolute move to and line
        to coordinates.  Place these coordinates into a list of polygon
        vertices.
        """

        if (not path) or (len(path) == 0):
            # Nothing to do
            return

        sp = Path(path)
        if (not sp) or (len(sp) == 0):
            # Path must have been devoid of any real content
            return

        # Get a cubic super path
        p = CubicSuperPath(sp)
        if (not p) or (len(p) == 0):
            # Probably never happens, but...
            return

        # Now traverse the cubic super path
        subpath_list = []
        subpath_vertices = []
        for sp in p:
            if len(subpath_vertices):
                # There's a prior subpath: see if it is closed and should be saved
                if distanceSquared(subpath_vertices[0], subpath_vertices[-1]) < 1:
                    # Keep the prior subpath: it appears to be a closed path
                    subpath_list.append(subpath_vertices)
            subpath_vertices = []
            subdivideCubicPath(sp, 0.2)
            for csp in sp:
                # Add this vertex to the list of vertices
                subpath_vertices.append(csp[1])

        # Handle final subpath
        if len(subpath_vertices):
            if distanceSquared(subpath_vertices[0], subpath_vertices[-1]) < 1:
                # Path appears to be closed so let's keep it
                subpath_list.append(subpath_vertices)

        # Empty path?
        if not subpath_list:
            return

        # Store the list of subpaths in a dictionary keyed off of the path's node pointer
        self.paths[node] = subpath_list
        self.paths_clone_transform[node] = clone_transform

    def recursivelyTraverseSvg(self, a_node_list, mat_current=None, parent_visibility='visible', clone_transform=None):

        """
        [ This too is largely lifted from eggbot.py ]

        Recursively walk the SVG document, building polygon vertex lists
        for each graphical element we support.

        Rendered SVG elements:
            <circle>, <ellipse>, <line>, <path>, <polygon>, <polyline>, <rect>

        Supported SVG elements:
            <group>, <use>

        Ignored SVG elements:
            <defs>, <eggbot>, <metadata>, <namedview>, <pattern>,
            processing directives

        All other SVG elements trigger an error (including <text>)
        """

        if mat_current is None:
            mat_current = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]

        for node in a_node_list:

            # Ignore invisible nodes
            v = node.get('visibility', parent_visibility)
            if v == 'inherit':
                v = parent_visibility
            if v == 'hidden' or v == 'collapse':
                pass

            # First apply the current matrix transform to this node's transform
            mat_new = Transform(mat_current) @ Transform(node.get("transform"))

            if node.tag in [inkex.addNS('g', 'svg'), 'g']:
                self.recursivelyTraverseSvg(node, mat_new, parent_visibility=v)

            elif node.tag in [inkex.addNS('use', 'svg'), 'use']:

                # A <use> element refers to another SVG element via an xlink:href="#blah"
                # attribute.  We will handle the element by doing an XPath search through
                # the document, looking for the element with the matching id="blah"
                # attribute.  We then recursively process that element after applying
                # any necessary (x,y) translation.
                #
                # Notes:
                #  1. We ignore the height and width attributes as they do not apply to
                #     path-like elements, and
                #  2. Even if the use element has visibility="hidden", SVG still calls
                #     for processing the referenced element.  The referenced element is
                #     hidden only if its visibility is "inherit" or "hidden".

                refid = node.get(inkex.addNS('href', 'xlink'))
                if not refid:
                    pass

                # [1:] to ignore leading '#' in reference
                path = '//*[@id="{}"]'.format(refid[1:])
                refnode = node.xpath(path)
                if refnode:
                    x = float(node.get('x', '0'))
                    y = float(node.get('y', '0'))
                    # Note: the transform has already been applied
                    if (x != 0) or (y != 0):
                        mat_new2 = composeTransform(mat_new, parseTransform('translate({:f},{:f})'.format(x, y)))
                    else:
                        mat_new2 = mat_new
                    v = node.get('visibility', v)
                    self.recursivelyTraverseSvg(refnode, mat_new2,
                                                parent_visibility=v, clone_transform=node.get('transform'))

            elif node.tag == inkex.addNS('path', 'svg'):
                path_data = node.get('d')
                if path_data:
                    self.addPathVertices(path_data, node, mat_new, clone_transform)

            elif node.tag in [inkex.addNS('rect', 'svg'), 'rect']:

                # Manually transform
                #
                #    <rect x="X" y="Y" width="W" height="H"/>
                #
                # into
                #
                #    <path d="MX,Y lW,0 l0,H l-W,0 z"/>
                #
                # I.e., explicitly draw three sides of the rectangle and the
                # fourth side implicitly

                # Create a path with the outline of the rectangle
                x = float(node.get('x'))
                y = float(node.get('y'))
                if (not x) or (not y):
                    pass
                w = float(node.get('width', '0'))
                h = float(node.get('height', '0'))
                a = []
                a.append(['M', [x, y]])
                a.append(['l', [w, 0]])
                a.append(['l', [0, h]])
                a.append(['l', [-w, 0]])
                a.append(['Z', []])
                self.addPathVertices(Path(a), node, mat_new, clone_transform)

            elif node.tag in [inkex.addNS('line', 'svg'), 'line']:

                # Convert
                #
                #   <line x1="X1" y1="Y1" x2="X2" y2="Y2/>
                #
                # to
                #
                #   <path d="MX1,Y1 LX2,Y2"/>

                x1 = float(node.get('x1'))
                y1 = float(node.get('y1'))
                x2 = float(node.get('x2'))
                y2 = float(node.get('y2'))
                if (not x1) or (not y1) or (not x2) or (not y2):
                    pass
                a = []
                a.append(['M ', [x1, y1]])
                a.append([' L ', [x2, y2]])
                self.addPathVertices(Path(a), node, mat_new, clone_transform)

            elif node.tag in [inkex.addNS('polyline', 'svg'), 'polyline']:

                # Convert
                #
                #  <polyline points="x1,y1 x2,y2 x3,y3 [...]"/>
                #
                # to
                #
                #   <path d="Mx1,y1 Lx2,y2 Lx3,y3 [...]"/>
                #
                # Note: we ignore polylines with no points

                pl = node.get('points', '').strip()
                if pl == '':
                    pass

                pa = pl.split()
                d = "".join(["M " + pa[i] if i == 0 else " L " + pa[i] for i in range(0, len(pa))])
                self.addPathVertices(d, node, mat_new, clone_transform)

            elif node.tag in [inkex.addNS('polygon', 'svg'), 'polygon']:

                # Convert
                #
                #  <polygon points="x1,y1 x2,y2 x3,y3 [...]"/>
                #
                # to
                #
                #   <path d="Mx1,y1 Lx2,y2 Lx3,y3 [...] Z"/>
                #
                # Note: we ignore polygons with no points

                pl = node.get('points', '').strip()
                if pl == '':
                    pass

                pa = pl.split()
                d = "".join(["M " + pa[i] if i == 0 else " L " + pa[i] for i in range(0, len(pa))])
                d += " Z"
                self.addPathVertices(d, node, mat_new, clone_transform)

            elif node.tag in [inkex.addNS('ellipse', 'svg'), 'ellipse',
                              inkex.addNS('circle', 'svg'), 'circle']:

                # Convert circles and ellipses to a path with two 180 degree arcs.
                # In general (an ellipse), we convert
                #
                #   <ellipse rx="RX" ry="RY" cx="X" cy="Y"/>
                #
                # to
                #
                #   <path d="MX1,CY A RX,RY 0 1 0 X2,CY A RX,RY 0 1 0 X1,CY"/>
                #
                # where
                #
                #   X1 = CX - RX
                #   X2 = CX + RX
                #
                # Note: ellipses or circles with a radius attribute of value 0 are ignored

                if node.tag in [inkex.addNS('ellipse', 'svg'),  'ellipse']:
                    rx = float(node.get('rx', '0'))
                    ry = float(node.get('ry', '0'))
                else:
                    rx = float(node.get('r', '0'))
                    ry = rx
                if rx == 0 or ry == 0:
                    pass

                cx = float(node.get('cx', '0'))
                cy = float(node.get('cy', '0'))
                x1 = cx - rx
                x2 = cx + rx
                d = 'M {x1:f},{cy:f} ' \
                    'A {rx:f},{ry:f} ' \
                    '0 1 0 {x2:f},{cy:f} ' \
                    'A {rx:f},{ry:f} ' \
                    '0 1 0 {x1:f},{cy:f}'.format(x1=x1,
                                                 x2=x2,
                                                 rx=rx,
                                                 ry=ry,
                                                 cy=cy)

                self.addPathVertices(d, node, mat_new, clone_transform)

            elif node.tag in [inkex.addNS('pattern', 'svg'), 'pattern']:
                pass

            elif node.tag in [inkex.addNS('metadata', 'svg'), 'metadata']:
                pass

            elif node.tag in [inkex.addNS('defs', 'svg'), 'defs']:
                pass

            elif node.tag in [inkex.addNS('namedview', 'sodipodi'), 'namedview']:
                pass

            elif node.tag in [inkex.addNS('eggbot', 'svg'), 'eggbot']:
                pass

            elif node.tag in [inkex.addNS('text', 'svg'), 'text']:
                inkex.errormsg('Warning: unable to draw text, please convert it to a path first.')
                pass

            elif not isinstance(node.tag, basestring):
                pass

            else:
                inkex.errormsg('Warning: unable to draw object <{}>, please convert it to a path first.'.format(node.tag))
                pass

    def joinWithNode(self, node, path, make_group=False, clone_transform=None):

        """
        Generate a SVG <path> element containing the path data "path".
        Then put this new <path> element into a <group> with the supplied
        node.  This means making a new <group> element and making the
        node a child of it with the new <path> as a sibling.
        """

        if (not path) or (len(path) == 0):
            return

        if make_group:
            # Make a new SVG <group> element whose parent is the parent of node
            parent = node.getparent()
            # was: if not parent:
            if parent is None:
                parent = self.document.getroot()
            g = etree.SubElement(parent, inkex.addNS('g', 'svg'))

            # Move node to be a child of this new <g> element
            g.append(node)

            # Promote the node's transform to the new parent group
            # This way, it will apply to the original paths and the
            # "twisted" paths
            transform = node.get('transform')
            if transform:
                g.set('transform', transform)
                del node.attrib['transform']
        else:
            g = node.getparent()

        # Now make a <path> element which contains the twist & is a child
        # of the new <g> element
        style = {'stroke': '#000000', 'fill': 'none', 'stroke-width': '1'}
        line_attribs = {'style': str(inkex.Style(style)), 'd': path}
        if (clone_transform is not None) and (clone_transform != ''):
            line_attribs['transform'] = clone_transform
        etree.SubElement(g, inkex.addNS('path', 'svg'), line_attribs)

    def twist(self, ratio):

        if not self.paths:
            return

        # Now iterate over all of the polygons
        for path in self.paths:
            for subpath in self.paths[path]:
                for i in range(len(subpath) - 1):
                    x = subpath[i][0] + ratio * (subpath[i + 1][0] - subpath[i][0])
                    y = subpath[i][1] + ratio * (subpath[i + 1][1] - subpath[i][1])
                    subpath[i] = [x, y]
                subpath[-1] = subpath[0]

    def draw(self, make_group=False):

        """
        Draw the edges of the current list of vertices
        """

        if not self.paths:
            return

        # Now iterate over all of the polygons
        for path in self.paths:
            for subpath in self.paths[path]:
                pdata = ''
                for vertex in subpath:
                    if pdata == '':
                        pdata = 'M {:f},{:f}'.format(vertex[0], vertex[1])
                    else:
                        pdata += ' L {:f},{:f}'.format(vertex[0], vertex[1])
                self.joinWithNode(path, pdata, make_group, self.paths_clone_transform[path])

    def effect(self):

        # Build a list of the vertices for the document's graphical elements
        if self.options.ids:
            # Traverse the selected objects
            for id_ in self.options.ids:
#                self.recursivelyTraverseSvg([self.svg.selected[id_]])
                self.recursivelyTraverseSvg([self.svg.getElementById(id_)])
        else:
            # Traverse the entire document
            self.recursivelyTraverseSvg(self.document.getroot())

        # Now iterate over the vertices N times
        for n in range(self.options.nSteps):
            self.twist(self.options.fRatio)
            self.draw(n == 0)


if __name__ == '__main__':
    Twist().run()