1
0
Fork 0
inkscape/share/extensions/path_mesh_m2p.py
Daniel Baumann 02d935e272
Adding upstream version 1.4.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-22 23:40:13 +02:00

346 lines
13 KiB
Python
Executable file

#!/usr/bin/env python3
#
# 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()