223 lines
8.4 KiB
Python
223 lines
8.4 KiB
Python
#!/usr/bin/env python3
|
|
#
|
|
# 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 PatternAlongPath(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__":
|
|
PatternAlongPath().run()
|