diff options
Diffstat (limited to 'share/extensions/path_mesh_m2p.py')
-rwxr-xr-x | share/extensions/path_mesh_m2p.py | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/share/extensions/path_mesh_m2p.py b/share/extensions/path_mesh_m2p.py new file mode 100755 index 0000000..cb4f8e2 --- /dev/null +++ b/share/extensions/path_mesh_m2p.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python +# +# Copyright (C) 2016 su_v, <suv-sf@users.sf.net> +# +# 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 +# +""" +Convert mesh gradient to path +""" + +import inkex +from inkex.elements import MeshGradient + +# globals +EPSILON = 1e-3 +MG_PROPS = [ + 'fill', + 'stroke' +] + +def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): + """Test approximate equality. + + ref: + PEP 485 -- A Function for testing approximate equality + https://www.python.org/dev/peps/pep-0485/#proposed-implementation + """ + # pylint: disable=invalid-name + return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) + + +def reverse_path(csp): + """Reverse path in CSP notation.""" + rcsp = [] + for subpath in reversed(csp): + rsub = [list(reversed(cp)) for cp in reversed(subpath)] + rcsp.append(rsub) + return rcsp + + +def join_path(csp1, sp1, csp2, sp2): + """Join sub-paths *sp1* and *sp2*.""" + pt1 = csp1[sp1][-1][1] + pt2 = csp2[sp2][0][1] + if (isclose(pt1[0], pt2[0], EPSILON) and + isclose(pt1[1], pt2[1], EPSILON)): + csp1[sp1][-1][2] = csp2[sp2][0][2] + csp1[sp1].extend(csp2[sp2][1:]) + else: + # inkex.debug('not close') + csp1.append(csp2[sp2]) + return csp1 + + +def is_url(val): + """Check whether attribute value is linked resource.""" + return val.startswith('url(#') + + +def mesh_corners(meshgradient): + """Return list of mesh patch corners, patch paths.""" + rows = len(meshgradient) + cols = len(meshgradient[0]) + # first corner of mesh gradient + corner_x = float(meshgradient.get('x', '0.0')) + corner_y = float(meshgradient.get('y', '0.0')) + # init corner and meshpatch lists + corners = [[None for _ in range(cols+1)] for _ in range(rows+1)] + corners[0][0] = [corner_x, corner_y] + meshpatch_csps = [] + for meshrow in range(rows): + for meshpatch in range(cols): + # get start point for current meshpatch edges + if meshrow == 0: + first_corner = corners[meshrow][meshpatch] + if meshrow > 0: + first_corner = corners[meshrow][meshpatch+1] + # parse path of meshpatch edges + path = 'M {},{}'.format(*first_corner) + for edge in meshgradient[meshrow][meshpatch]: + path = ' '.join([path, edge.get('path')]) + csp = inkex.Path(path).to_superpath() + # update corner list with current meshpatch + if meshrow == 0: + corners[meshrow][meshpatch+1] = csp[0][1][1] + corners[meshrow+1][meshpatch+1] = csp[0][2][1] + if meshpatch == 0: + corners[meshrow+1][meshpatch] = csp[0][3][1] + if meshrow > 0: + corners[meshrow][meshpatch+1] = csp[0][0][1] + corners[meshrow+1][meshpatch+1] = csp[0][1][1] + if meshpatch == 0: + corners[meshrow+1][meshpatch] = csp[0][2][1] + # append to list of meshpatch csp + meshpatch_csps.append(csp) + return corners, meshpatch_csps + + +def mesh_hvlines(meshgradient): + """Return lists of vertical and horizontal patch edges.""" + rows = len(meshgradient) + cols = len(meshgradient[0]) + # init lists for horizontal, vertical lines + hlines = [[None for _ in range(cols)] for _ in range(rows+1)] + vlines = [[None for _ in range(rows)] for _ in range(cols+1)] + for meshrow in range(rows): + for meshpatch in range(cols): + # horizontal edges + if meshrow == 0: + edge = meshgradient[meshrow][meshpatch][0] + hlines[meshrow][meshpatch] = edge.get('path') + edge = meshgradient[meshrow][meshpatch][2] + hlines[meshrow+1][meshpatch] = edge.get('path') + if meshrow > 0: + edge = meshgradient[meshrow][meshpatch][1] + hlines[meshrow+1][meshpatch] = edge.get('path') + # vertical edges + if meshrow == 0: + edge = meshgradient[meshrow][meshpatch][1] + vlines[meshpatch+1][meshrow] = edge.get('path') + if meshpatch == 0: + edge = meshgradient[meshrow][meshpatch][3] + vlines[meshpatch][meshrow] = edge.get('path') + if meshrow > 0: + edge = meshgradient[meshrow][meshpatch][0] + vlines[meshpatch+1][meshrow] = edge.get('path') + if meshpatch == 0: + edge = meshgradient[meshrow][meshpatch][2] + vlines[meshpatch][meshrow] = edge.get('path') + return hlines, vlines + + +def mesh_to_outline(corners, hlines, vlines): + """Construct mesh outline as CSP path.""" + outline_csps = [] + path = 'M {},{}'.format(*corners[0][0]) + for edge_path in hlines[0]: + path = ' '.join([path, edge_path]) + for edge_path in vlines[-1]: + path = ' '.join([path, edge_path]) + for edge_path in reversed(hlines[-1]): + path = ' '.join([path, edge_path]) + for edge_path in reversed(vlines[0]): + path = ' '.join([path, edge_path]) + outline_csps.append(inkex.Path(path).to_superpath()) + return outline_csps + + +def mesh_to_grid(corners, hlines, vlines): + """Construct mesh grid with CSP paths.""" + rows = len(corners) - 1 + cols = len(corners[0]) - 1 + gridline_csps = [] + # horizontal + path = 'M {},{}'.format(*corners[0][0]) + for edge_path in hlines[0]: + path = ' '.join([path, edge_path]) + gridline_csps.append(inkex.Path(path).to_superpath()) + for i in range(1, rows+1): + path = 'M {},{}'.format(*corners[i][-1]) + for edge_path in reversed(hlines[i]): + path = ' '.join([path, edge_path]) + gridline_csps.append(inkex.Path(path).to_superpath()) + # vertical + path = 'M {},{}'.format(*corners[-1][0]) + for edge_path in reversed(vlines[0]): + path = ' '.join([path, edge_path]) + gridline_csps.append(inkex.Path(path).to_superpath()) + for j in range(1, cols+1): + path = 'M {},{}'.format(*corners[0][j]) + for edge_path in vlines[j]: + path = ' '.join([path, edge_path]) + gridline_csps.append(inkex.Path(path).to_superpath()) + return gridline_csps + + +def mesh_to_faces(corners, hlines, vlines): + """Construct mesh faces with CSP paths.""" + rows = len(corners) - 1 + cols = len(corners[0]) - 1 + face_csps = [] + for row in range(rows): + for col in range(cols): + # init new face + face = [] + # init edge paths + edge_t = hlines[row][col] + edge_b = hlines[row+1][col] + edge_l = vlines[col][row] + edge_r = vlines[col+1][row] + # top edge, first + if row == 0: + path = 'M {},{}'.format(*corners[row][col]) + path = ' '.join([path, edge_t]) + face.append(inkex.Path(path).to_superpath()[0]) + else: + path = 'M {},{}'.format(*corners[row][col+1]) + path = ' '.join([path, edge_t]) + face.append(reverse_path(inkex.Path(path).to_superpath())[0]) + # right edge + path = 'M {},{}'.format(*corners[row][col+1]) + path = ' '.join([path, edge_r]) + join_path(face, -1, inkex.Path(path).to_superpath(), 0) + # bottom edge + path = 'M {},{}'.format(*corners[row+1][col+1]) + path = ' '.join([path, edge_b]) + join_path(face, -1, inkex.Path(path).to_superpath(), 0) + # left edge + if col == 0: + path = 'M {},{}'.format(*corners[row+1][col]) + path = ' '.join([path, edge_l]) + join_path(face, -1, inkex.Path(path).to_superpath(), 0) + else: + path = 'M {},{}'.format(*corners[row][col]) + path = ' '.join([path, edge_l]) + join_path(face, -1, reverse_path(inkex.Path(path).to_superpath()), 0) + # append face to output list + face_csps.append(face) + return face_csps + + +class MeshToPath(inkex.EffectExtension): + """Effect extension to convert mesh geometry to path data.""" + def add_arguments(self, pars): + pars.add_argument("--tab", help="The selected UI-tab") + pars.add_argument("--mode", default="outline", help="Edge mode") + + def process_props(self, mdict, res_type='meshgradient'): + """Process style properties of style dict *mdict*.""" + result = [] + for key, val in mdict.items(): + if key in MG_PROPS: + if is_url(val): + paint_server = self.svg.getElementById(val) + if res_type == 'meshgradient' and isinstance(paint_server, MeshGradient): + result.append(paint_server) + return result + + def process_style(self, node, res_type='meshgradient'): + """Process style of *node*.""" + result = [] + # Presentation attributes + adict = dict(node.attrib) + result.extend(self.process_props(adict, res_type)) + # Inline CSS style properties + result.extend(self.process_props(node.style, res_type)) + # TODO: check for child paint servers + return result + + def find_meshgradients(self, node): + """Parse node style, return list with linked meshgradients.""" + return self.process_style(node, res_type='meshgradient') + + # ----- Process meshgradient definitions + + def mesh_to_csp(self, meshgradient): + """Parse mesh geometry and build csp-based path data.""" + + # init variables + transform = None + mode = self.options.mode + + # gradient units + mesh_units = meshgradient.get('gradientUnits', 'objectBoundingBox') + if mesh_units == 'objectBoundingBox': + # TODO: position and scale based on "objectBoundingBox" units + return + + # Inkscape SVG 0.92 and SVG 2.0 draft mesh transformations + transform = meshgradient.gradientTransform * meshgradient.transform + + # parse meshpatches, calculate absolute corner coords + corners, meshpatch_csps = mesh_corners(meshgradient) + + if mode == 'meshpatches': + return meshpatch_csps, transform + else: + hlines, vlines = mesh_hvlines(meshgradient) + if mode == 'outline': + return mesh_to_outline(corners, hlines, vlines), transform + elif mode == 'gridlines': + return mesh_to_grid(corners, hlines, vlines), transform + elif mode == 'faces': + return mesh_to_faces(corners, hlines, vlines), transform + + # ----- Convert meshgradient definitions + + def csp_to_path(self, node, csp_list, transform=None): + """Create new paths based on csp data, return group with paths.""" + # set up stroke width, group + stroke_width = self.svg.unittouu('1px') + stroke_color = '#000000' + style = { + 'fill': 'none', + 'stroke': stroke_color, + 'stroke-width': str(stroke_width), + } + + group = inkex.Group() + # apply gradientTransform and node's preserved transform to group + group.transform = transform * node.transform + + # convert each csp to path, append to group + for csp in csp_list: + elem = group.add(inkex.PathElement()) + elem.style = style + elem.path = inkex.CubicSuperPath(csp) + if self.options.mode == 'outline': + elem.path.close() + elif self.options.mode == 'faces': + if len(csp) == 1 and len(csp[0]) == 5: + elem.path.close() + return group + + def effect(self): + """Main routine to convert mesh geometry to path data.""" + # loop through selection + for node in self.svg.selected.values(): + meshgradients = self.find_meshgradients(node) + # if style references meshgradient + if meshgradients: + for meshgradient in meshgradients: + csp_list = None + result = None + # parse mesh geometry + if meshgradient is not None: + csp_list, mat = self.mesh_to_csp(meshgradient) + # generate new paths with path data based on mesh geometry + if csp_list is not None: + result = self.csp_to_path(node, csp_list, mat) + # add result (group) to document + if result is not None: + index = node.getparent().index(node) + node.getparent().insert(index+1, result) + + +if __name__ == '__main__': + MeshToPath().run() |