diff options
Diffstat (limited to 'share/extensions/pathscatter.py')
-rwxr-xr-x | share/extensions/pathscatter.py | 235 |
1 files changed, 235 insertions, 0 deletions
diff --git a/share/extensions/pathscatter.py b/share/extensions/pathscatter.py new file mode 100755 index 0000000..7ffee63 --- /dev/null +++ b/share/extensions/pathscatter.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python +# coding=utf-8 +# +# 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 scatters an object (the pattern) along other paths (skeletons)... +The topmost selected object is the pattern +the other selected ones are the skeletons. + +No deformation is applied to the pattern itself. +""" +import random +import math + +import inkex +from inkex import bezier, Transform, BoundingBox, Group, Use +from inkex.localization import inkex_gettext as _ + +import pathmodifier + + +class PathScatter(pathmodifier.Diffeo): + def __init__(self): + super(PathScatter, self).__init__() + self.arg_parser.add_argument( + "-n", + "--noffset", + type=float, + dest="noffset", + default=0.0, + help="normal offset", + ) + self.arg_parser.add_argument( + "-t", + "--toffset", + type=float, + dest="toffset", + default=0.0, + help="tangential offset", + ) + self.arg_parser.add_argument( + "-g", + "--grouppick", + type=inkex.Boolean, + dest="grouppick", + default=False, + help="if pattern is a group then randomly pick group members", + ) + self.arg_parser.add_argument( + "-m", + "--pickmode", + type=str, + dest="pickmode", + default="rand", + help="group pick mode (rand=random seq=sequentially)", + ) + self.arg_parser.add_argument( + "-f", + "--follow", + type=inkex.Boolean, + dest="follow", + default=True, + help="choose between wave or snake effect", + ) + self.arg_parser.add_argument( + "-s", + "--stretch", + type=inkex.Boolean, + dest="stretch", + default=False, + help="repeat the path to fit deformer's length", + ) + self.arg_parser.add_argument( + "-p", "--space", type=float, dest="space", default=0.0 + ) + self.arg_parser.add_argument( + "-r", + "--rotate", + type=inkex.Boolean, + dest="vertical", + default=False, + help="reference path is vertical", + ) + self.arg_parser.add_argument( + "-c", + "--copymode", + type=str, + dest="copymode", + default="move", + help="""How the pattern is duplicated. Default: 'move', + Options: 'clone', 'duplicate', 'move'""", + ) + self.arg_parser.add_argument( + "--tab", + type=str, + dest="tab", + help="The selected UI-tab when OK was pressed", + ) + + def localTransformAt(self, s, skelcomp, lengths, isclosed, follow=True): + """ + receives a length, and returns the corresponding point and tangent of skelcomp + if follow is set to false, returns only the translation + """ + i, t = self.lengthtotime(s, lengths, isclosed) + if i == len(skelcomp) - 1: + x, y = bezier.between_point(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 = bezier.between_point(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] + if follow: + mat = [[dx, -dy, x], [dy, dx, y]] + else: + mat = [[1, 0, x], [0, 1, y]] + return Transform(mat) + + def center_node_at_origin(self, node): + """Translates a node to the origin and applies translation if requested""" + bbox = node.bounding_box() + mat = Transform([[1, 0, -bbox.center.x], [0, 1, -bbox.center.y]]) + if self.options.vertical: + bbox = BoundingBox(-bbox.y, -bbox.x) + mat = Transform([[0, -1, 0], [1, 0, 0]]) @ mat + mat.add_translate([0, self.options.noffset]) + node.transform = mat @ node.transform + return bbox + + def effect(self): + + if len(self.svg.selection) < 2: + inkex.errormsg(_("This extension requires two selected paths.")) + return + original_pattern_node, skeletons = self.get_patterns_and_skeletons(False, False) + + g_node = Group() + original_pattern_node.getparent().append(g_node) + + if self.options.copymode == "clone": + pattern_node = g_node.add(Use()) + pattern_node.href = original_pattern_node + else: + pattern_node = original_pattern_node.duplicate() + + # We will later compute transforms relative to the origin + bbox = self.center_node_at_origin(pattern_node) + + width = bbox.width + dx = width + self.options.space + if dx < 0.01: + if isinstance(original_pattern_node, inkex.TextElement): + raise inkex.AbortExtension(_("Please convert texts to path first")) + raise inkex.AbortExtension( + _( + "The total length of the pattern is too small\n" + "Please choose a larger object or set 'Space between copies' > 0" + ) + ) + + # check if group and expand it + pattern_list = [] + if self.options.grouppick and isinstance(pattern_node, Group): + mat = pattern_node.transform + for child in pattern_node: + child.transform = mat @ child.transform + pattern_list.append(child) + else: + pattern_list.append(pattern_node) + + self._do_transform(skeletons, width, pattern_list, g_node) + + if self.options.copymode == "move": + original_pattern_node.getparent().remove(original_pattern_node) + # pattern_node was just a temporary copy, definitely remove this + pattern_node.getparent().remove(pattern_node) + + def _do_transform(self, skeletons, width, pattern_list, g_node): + counter = 0 + for skelnode in skeletons.values(): + skelnode.apply_transform() + cur_skeleton = skelnode.path.to_superpath() + for comp in cur_skeleton: + skelcomp, lengths = self.linearize(comp) + skel_closed = all( + [math.isclose(i, j) for i, j in zip(skelcomp[0], skelcomp[-1])] + ) + + length = sum(lengths) + dx = width + self.options.space + if self.options.stretch: + n = int((length + self.options.space) / dx) + if n > 0: + dx = (length) / n + + s = 0 if self.options.stretch else self.options.toffset * 0.01 * dx + while s <= length: + local_transform = self.localTransformAt( + s, skelcomp, lengths, skel_closed, self.options.follow + ) + + pattern_idx = ( + random.randint(0, len(pattern_list) - 1) + if self.options.pickmode == "rand" + else counter % len(pattern_list) + ) + + clone = pattern_list[pattern_idx].copy() + + g_node.append(clone) + + clone.transform = local_transform @ clone.transform + s += dx + counter += 1 + + +if __name__ == "__main__": + PathScatter().run() |