summaryrefslogtreecommitdiffstats
path: root/share/extensions/path_mesh_m2p.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xshare/extensions/path_mesh_m2p.py346
1 files changed, 346 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..cb5425b
--- /dev/null
+++ b/share/extensions/path_mesh_m2p.py
@@ -0,0 +1,346 @@
+#!/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 = node.specified_style()
+ result = self.process_props(result, 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.viewport_to_unit("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.selection.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()