summaryrefslogtreecommitdiffstats
path: root/share/extensions/pathalongpath.py
diff options
context:
space:
mode:
Diffstat (limited to 'share/extensions/pathalongpath.py')
-rwxr-xr-xshare/extensions/pathalongpath.py222
1 files changed, 222 insertions, 0 deletions
diff --git a/share/extensions/pathalongpath.py b/share/extensions/pathalongpath.py
new file mode 100755
index 0000000..9c4bac5
--- /dev/null
+++ b/share/extensions/pathalongpath.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr
+# 2021 Jonathan Neuhauser, jonathan.neuhauser@outlook.com
+#
+# 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 math
+
+import inkex
+from inkex.bezier import tpoint
+from inkex.paths import CubicSuperPath
+from inkex.localization import inkex_gettext as _
+
+import pathmodifier
+
+
+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=True,
+ help="duplicate pattern before deformation",
+ )
+ pars.add_argument("--tab", help="The selected UI-tab when OK was pressed")
+
+ def apply_diffeomorphism(self, bpt, skelcomp, lengths, isclosed, 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] - skelcomp[0][0]
+ i, t = self.lengthtotime(s, lengths, isclosed)
+ if i == len(skelcomp) - 1:
+ x, y = tpoint(skelcomp[i - 1], skelcomp[i], 1 + t)
+ dx = (skelcomp[i][0] - skelcomp[i - 1][0]) / lengths[-1]
+ dy = (skelcomp[i][1] - skelcomp[i - 1][1]) / lengths[-1]
+ else:
+ x, y = tpoint(skelcomp[i], skelcomp[i + 1], t)
+ dx = (skelcomp[i + 1][0] - skelcomp[i][0]) / lengths[i]
+ dy = (skelcomp[i + 1][1] - skelcomp[i][1]) / lengths[i]
+
+ vx = 0
+ vy = bpt[1] - 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] - skelcomp[0][0] - s
+ vy = v[1] - 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
+
+ patterns, skels = self.get_patterns_and_skeletons(True, self.options.duplicate)
+ bboxes = [pattern.bounding_box() for pattern in patterns.values()]
+ if None in bboxes: # for texts, we can't compute the bounding box
+ raise inkex.AbortExtension(_("Please convert texts to path first"))
+ bbox = sum(bboxes, None)
+
+ 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"
+ )
+ )
+ for pattern in patterns.values():
+ if isinstance(pattern, inkex.PathElement):
+ pattern.apply_transform()
+ pattern.path = self._do_transform(
+ skels, pattern.path.to_superpath(), delta_x, bbox
+ )
+
+ def _do_transform(self, skeletons, p0, dx, bbox):
+ if self.options.vertical:
+ self.flipxy(p0)
+ newp = []
+ for skelnode in skeletons.values():
+ skelnode.apply_transform()
+ cur_skeleton = skelnode.path.to_superpath()
+ if self.options.vertical:
+ self.flipxy(cur_skeleton)
+ for comp in cur_skeleton:
+ path = copy.deepcopy(p0)
+ skelcomp, lengths = self.linearize(comp)
+
+ skel_closed = all(
+ [math.isclose(i, j) for i, j in zip(skelcomp[0], skelcomp[-1])]
+ )
+
+ length = sum(lengths)
+ xoffset = skelcomp[0][0] - bbox.x.minimum + self.options.toffset
+ yoffset = skelcomp[0][1] - bbox.y.center - self.options.noffset
+ if self.options.repeat:
+ nb_copies = max(1, int(round((length + self.options.space) / dx)))
+ width = dx * nb_copies
+ if not skel_closed:
+ width -= self.options.space
+ bbox.x.maximum = bbox.x.minimum + width
+ new = []
+ for sub in path:
+ for _ in range(nb_copies):
+ new.append(copy.deepcopy(sub))
+ self.offset(sub, dx, 0)
+ path = new
+
+ for sub in path:
+ self.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:
+ self.stretch(sub, length / bbox.width, 1, skelcomp[0])
+
+ for sub in path:
+ for ctlpt in sub:
+ self.apply_diffeomorphism(
+ ctlpt[1],
+ skelcomp,
+ lengths,
+ skel_closed,
+ (ctlpt[0], ctlpt[2]),
+ )
+
+ if self.options.vertical:
+ self.flipxy(path)
+ newp += path
+ return CubicSuperPath(newp)
+
+
+if __name__ == "__main__":
+ PathAlongPath().run()