summaryrefslogtreecommitdiffstats
path: root/share/extensions/pathalongpath.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 16:29:01 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 16:29:01 +0000
commit35a96bde514a8897f6f0fcc41c5833bf63df2e2a (patch)
tree657d15a03cc46bd099fc2c6546a7a4ad43815d9f /share/extensions/pathalongpath.py
parentInitial commit. (diff)
downloadinkscape-upstream.tar.xz
inkscape-upstream.zip
Adding upstream version 1.0.2.upstream/1.0.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'share/extensions/pathalongpath.py')
-rwxr-xr-xshare/extensions/pathalongpath.py265
1 files changed, 265 insertions, 0 deletions
diff --git a/share/extensions/pathalongpath.py b/share/extensions/pathalongpath.py
new file mode 100755
index 0000000..5ac4286
--- /dev/null
+++ b/share/extensions/pathalongpath.py
@@ -0,0 +1,265 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr
+#
+# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+# barraud@math.univ-lille1.fr
+#
+"""
+This script deforms an object (the pattern) along other paths (skeletons)...
+The first selected object is the pattern
+the last selected ones are the skeletons.
+
+Imagine a straight horizontal line L in the middle of the bounding box of the pattern.
+Consider the normal bundle of L: the collection of all the vertical lines meeting L.
+Consider this as the initial state of the plane; in particular, think of the pattern
+as painted on these lines.
+
+Now move and bend L to make it fit a skeleton, and see what happens to the normals:
+they move and rotate, deforming the pattern.
+"""
+import copy
+
+import inkex
+from inkex.bezier import pointdistance, beziersplitatt, tpoint
+from inkex.paths import CubicSuperPath
+
+import pathmodifier
+
+def flipxy(path):
+ for pathcomp in path:
+ for ctl in pathcomp:
+ for pt in ctl:
+ tmp = pt[0]
+ pt[0] = -pt[1]
+ pt[1] = -tmp
+
+
+def offset(pathcomp, dx, dy):
+ for ctl in pathcomp:
+ for pt in ctl:
+ pt[0] += dx
+ pt[1] += dy
+
+
+def stretch(pathcomp, xscale, yscale, org):
+ for ctl in pathcomp:
+ for pt in ctl:
+ pt[0] = org[0] + (pt[0] - org[0]) * xscale
+ pt[1] = org[1] + (pt[1] - org[1]) * yscale
+
+
+def linearize(p, tolerance=0.001):
+ """
+ This function receives a component of a 'cubicsuperpath' and returns two things:
+ The path subdivided in many straight segments, and an array containing the length of each segment.
+
+ We could work with bezier path as well, but bezier arc lengths are (re)computed for each point
+ in the deformed object. For complex paths, this might take a while.
+ """
+ zero = 0.000001
+ i = 0
+ d = 0
+ lengths = []
+ while i < len(p) - 1:
+ box = pointdistance(p[i][1], p[i][2])
+ box += pointdistance(p[i][2], p[i + 1][0])
+ box += pointdistance(p[i + 1][0], p[i + 1][1])
+ chord = pointdistance(p[i][1], p[i + 1][1])
+ if (box - chord) > tolerance:
+ b1, b2 = beziersplitatt([p[i][1], p[i][2], p[i + 1][0], p[i + 1][1]], 0.5)
+ p[i][2][0], p[i][2][1] = b1[1]
+ p[i + 1][0][0], p[i + 1][0][1] = b2[2]
+ p.insert(i + 1, [[b1[2][0], b1[2][1]], [b1[3][0], b1[3][1]], [b2[1][0], b2[1][1]]])
+ else:
+ d = (box + chord) / 2
+ lengths.append(d)
+ i += 1
+ new = [p[i][1] for i in range(0, len(p) - 1) if lengths[i] > zero]
+ new.append(p[-1][1])
+ lengths = [l for l in lengths if l > zero]
+ return new, lengths
+
+
+class PathAlongPath(pathmodifier.PathModifier):
+ """Deform a path along a second path"""
+ def add_arguments(self, pars):
+ pars.add_argument("-n", "--noffset", type=float, default=0.0, help="normal offset")
+ pars.add_argument("-t", "--toffset", type=float, default=0.0, help="tangential offset")
+ pars.add_argument("-k", "--kind", type=str, default='')
+ pars.add_argument("-c", "--copymode", default="Single",
+ help="repeat the path to fit deformer's length")
+ pars.add_argument("-p", "--space", type=float, default=0.0)
+ pars.add_argument("-v", "--vertical", type=inkex.Boolean, default=False,
+ help="reference path is vertical")
+ pars.add_argument("-d", "--duplicate", type=inkex.Boolean, default=False,
+ help="duplicate pattern before deformation")
+ pars.add_argument("--tab", help="The selected UI-tab when OK was pressed")
+
+ def prepare_selection(self):
+ """
+ first selected->pattern, all but first selected-> skeletons
+ """
+ skeletons = self.svg.selection.paint_order()
+
+ elem = skeletons.pop()
+ if self.options.duplicate:
+ elem = elem.duplicate()
+ pattern = elem.to_path_element()
+ elem.replace_with(pattern)
+
+ self.expand_clones(skeletons, True, False)
+ self.objects_to_paths(skeletons)
+ return pattern, skeletons.id_dict()
+
+ def lengthtotime(self, l):
+ """
+ Receives an arc length l, and returns the index of the segment in self.skelcomp
+ containing the corresponding point, to gether with the position of the point on this segment.
+
+ If the deformer is closed, do computations modulo the toal length.
+ """
+ if self.skelcompIsClosed:
+ l = l % sum(self.lengths)
+ if l <= 0:
+ return 0, l / self.lengths[0]
+ i = 0
+ while (i < len(self.lengths)) and (self.lengths[i] <= l):
+ l -= self.lengths[i]
+ i += 1
+ t = l / self.lengths[min(i, len(self.lengths) - 1)]
+ return i, t
+
+ def apply_diffeomorphism(self, bpt, vects=()):
+ """
+ The kernel of this stuff:
+ bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
+ """
+ s = bpt[0] - self.skelcomp[0][0]
+ i, t = self.lengthtotime(s)
+ if i == len(self.skelcomp) - 1:
+ x, y = tpoint(self.skelcomp[i - 1], self.skelcomp[i], 1 + t)
+ dx = (self.skelcomp[i][0] - self.skelcomp[i - 1][0]) / self.lengths[-1]
+ dy = (self.skelcomp[i][1] - self.skelcomp[i - 1][1]) / self.lengths[-1]
+ else:
+ x, y = tpoint(self.skelcomp[i], self.skelcomp[i + 1], t)
+ dx = (self.skelcomp[i + 1][0] - self.skelcomp[i][0]) / self.lengths[i]
+ dy = (self.skelcomp[i + 1][1] - self.skelcomp[i][1]) / self.lengths[i]
+
+ vx = 0
+ vy = bpt[1] - self.skelcomp[0][1]
+ if self.options.wave:
+ bpt[0] = x + vx * dx
+ bpt[1] = y + vy + vx * dy
+ else:
+ bpt[0] = x + vx * dx - vy * dy
+ bpt[1] = y + vx * dy + vy * dx
+
+ for v in vects:
+ vx = v[0] - self.skelcomp[0][0] - s
+ vy = v[1] - self.skelcomp[0][1]
+ if self.options.wave:
+ v[0] = x + vx * dx
+ v[1] = y + vy + vx * dy
+ else:
+ v[0] = x + vx * dx - vy * dy
+ v[1] = y + vx * dy + vy * dx
+
+ def effect(self):
+ if len(self.options.ids) < 2:
+ raise inkex.AbortExtension("This extension requires two selected paths.")
+
+ self.options.wave = (self.options.kind == "Ribbon")
+ if self.options.copymode == "Single":
+ self.options.repeat = False
+ self.options.stretch = False
+ elif self.options.copymode == "Repeated":
+ self.options.repeat = True
+ self.options.stretch = False
+ elif self.options.copymode == "Single, stretched":
+ self.options.repeat = False
+ self.options.stretch = True
+ elif self.options.copymode == "Repeated, stretched":
+ self.options.repeat = True
+ self.options.stretch = True
+
+ pattern, skels = self.prepare_selection()
+ bbox = pattern.bounding_box()
+
+ if self.options.vertical:
+ # flipxy(bbox)...
+ bbox = inkex.BoundingBox(-bbox.y, -bbox.x)
+
+ width = bbox.width
+ delta_x = width + self.options.space
+ if delta_x < 0.01:
+ raise inkex.AbortExtension("The total length of the pattern is too small\n"\
+ "Please choose a larger object or set 'Space between copies' > 0")
+
+ if isinstance(pattern, inkex.PathElement):
+ pattern.path = self._sekl_call(skels, pattern.path.to_superpath(), delta_x, bbox)
+
+ def _sekl_call(self, skeletons, p0, dx, bbox):
+ if self.options.vertical:
+ flipxy(p0)
+ newp = []
+ for skelnode in skeletons.values():
+ self.curSekeleton = skelnode.path.to_superpath()
+ if self.options.vertical:
+ flipxy(self.curSekeleton)
+ for comp in self.curSekeleton:
+ path = copy.deepcopy(p0)
+ self.skelcomp, self.lengths = linearize(comp)
+ # !!!!>----> TODO: really test if path is closed! end point==start point is not enough!
+ self.skelcompIsClosed = (self.skelcomp[0] == self.skelcomp[-1])
+
+ length = sum(self.lengths)
+ xoffset = self.skelcomp[0][0] - bbox.x.minimum + self.options.toffset
+ yoffset = self.skelcomp[0][1] - bbox.y.center - self.options.noffset
+
+ if self.options.repeat:
+ NbCopies = max(1, int(round((length + self.options.space) / dx)))
+ width = dx * NbCopies
+ if not self.skelcompIsClosed:
+ width -= self.options.space
+ bbox.x.maximum = bbox.x.minimum + width
+ new = []
+ for sub in path:
+ for _ in range(NbCopies):
+ new.append(copy.deepcopy(sub))
+ offset(sub, dx, 0)
+ path = new
+
+ for sub in path:
+ offset(sub, xoffset, yoffset)
+
+ if self.options.stretch:
+ if not bbox.width:
+ raise inkex.AbortExtension("The 'stretch' option requires that the pattern must have non-zero width :\nPlease edit the pattern width.")
+ for sub in path:
+ stretch(sub, length / bbox.width, 1, self.skelcomp[0])
+
+ for sub in path:
+ for ctlpt in sub:
+ self.apply_diffeomorphism(ctlpt[1], (ctlpt[0], ctlpt[2]))
+
+ if self.options.vertical:
+ flipxy(path)
+ newp += path
+ return CubicSuperPath(newp)
+
+
+if __name__ == '__main__':
+ PathAlongPath().run()