mightyscape-1.2/extensions/fablabchemnitz/twist/twist.py

518 lines
19 KiB
Python
Raw Normal View History

#!/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()