159 lines
6.2 KiB
Python
Executable file
159 lines
6.2 KiB
Python
Executable file
# coding=utf-8
|
|
#
|
|
# Copyright (C) 2005 Aaron Spike, aaron@ekips.org
|
|
#
|
|
# 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.
|
|
#
|
|
|
|
import inkex
|
|
from inkex.transforms import DirectedLineSegment
|
|
from inkex.localization import inkex_gettext as _
|
|
from operator import truediv
|
|
|
|
|
|
class Envelope(inkex.EffectExtension):
|
|
"""Distort a path/group of paths to a second path"""
|
|
|
|
def effect(self):
|
|
if len(self.svg.selection) != 2:
|
|
raise inkex.AbortExtension(_("You must select two objects only."))
|
|
|
|
obj, envelope = self.svg.selection
|
|
|
|
if isinstance(obj, (inkex.PathElement, inkex.Group)):
|
|
if isinstance(envelope, inkex.PathElement):
|
|
# Get bounding box plus any extra composed transform of parents.
|
|
bbox = obj.bounding_box(obj.getparent().composed_transform())
|
|
|
|
# distill trafo into four node points
|
|
path = envelope.path.transform(
|
|
envelope.composed_transform()
|
|
).to_superpath()
|
|
tbox = self.envelope_box_from_path(path)
|
|
else:
|
|
if isinstance(envelope, inkex.Group):
|
|
raise inkex.AbortExtension(
|
|
_(
|
|
"The second selected object is a group, not a"
|
|
" path.\nTry using Object->Ungroup."
|
|
)
|
|
)
|
|
raise inkex.AbortExtension(
|
|
_(
|
|
"The second selected object is not a path.\nTry using"
|
|
" the procedure Path->Object to Path."
|
|
)
|
|
)
|
|
else:
|
|
raise inkex.AbortExtension(
|
|
_(
|
|
"The first selected object is neither a path nor a group.\nTry using"
|
|
" the procedure Path->Object to Path."
|
|
)
|
|
)
|
|
|
|
self.process_object(obj, tbox, bbox)
|
|
|
|
def envelope_box_from_path(self, envelope_path):
|
|
if len(envelope_path) < 1 or len(envelope_path[0]) < 4:
|
|
raise inkex.AbortExtension(
|
|
_("Second selected path is too short. Must be four or more nodes.")
|
|
)
|
|
trafo = [[(csp[1][0], csp[1][1]) for csp in subs] for subs in envelope_path][0][
|
|
:4
|
|
]
|
|
# vectors pointing away from the trafo origin
|
|
tbox = [
|
|
DirectedLineSegment(trafo[0], trafo[1]),
|
|
DirectedLineSegment(trafo[1], trafo[2]),
|
|
DirectedLineSegment(trafo[3], trafo[2]),
|
|
DirectedLineSegment(trafo[0], trafo[3]),
|
|
]
|
|
vects = [segment.vector for segment in tbox]
|
|
if (
|
|
0.0
|
|
== vects[0].cross(vects[1])
|
|
== vects[1].cross(vects[2])
|
|
== vects[2].cross(vects[3])
|
|
):
|
|
raise inkex.AbortExtension(
|
|
_("The points for the selected envelope must not all be in a line.")
|
|
)
|
|
return tbox
|
|
|
|
def process_object(self, obj, tbox, bbox):
|
|
if isinstance(obj, inkex.PathElement):
|
|
self.process_path(obj, tbox, bbox)
|
|
elif isinstance(obj, inkex.Group):
|
|
self.process_group(obj, tbox, bbox)
|
|
|
|
def process_group(self, group, tbox, bbox):
|
|
"""Go through all groups to process all paths inside them"""
|
|
for node in group:
|
|
self.process_object(node, tbox, bbox)
|
|
|
|
def process_path(self, element, tbox, bbox):
|
|
# Get out path's absolute and root coordinates, so obj and envelope
|
|
# are always in the same coordinate system.
|
|
points = (
|
|
element.path.to_absolute()
|
|
.transform(element.composed_transform())
|
|
.to_superpath()
|
|
)
|
|
|
|
for subs in points:
|
|
for csp in subs:
|
|
csp[0] = self.transform_point(tbox, bbox, *csp[0])
|
|
csp[1] = self.transform_point(tbox, bbox, *csp[1])
|
|
csp[2] = self.transform_point(tbox, bbox, *csp[2])
|
|
|
|
# Put the modified path back, undo the root transformation
|
|
element.path = points.to_path().transform(-element.composed_transform())
|
|
|
|
@staticmethod
|
|
def transform_point(tbox, bbox, x, y):
|
|
"""Transform algorithm thanks to Jose Hevia (freon)"""
|
|
vector = (x, y) - bbox.minimum
|
|
xratio, yratio = map(truediv, vector, (bbox.width, bbox.height))
|
|
horz = DirectedLineSegment(
|
|
tbox[0].point_at_ratio(xratio), tbox[2].point_at_ratio(xratio)
|
|
)
|
|
vert = DirectedLineSegment(
|
|
tbox[3].point_at_ratio(yratio), tbox[1].point_at_ratio(yratio)
|
|
)
|
|
denom = horz.vector.cross(vert.vector)
|
|
if denom == 0.0:
|
|
# Degenerate cases of intersecting envelope edges
|
|
if horz.length == 0.0:
|
|
return horz.start
|
|
if vert.length == 0.0:
|
|
return vert.start
|
|
# Here we should know that the lines share a start or end point.
|
|
if horz.vector.dot(vert.vector) < 0:
|
|
# Segments point opposite directions
|
|
if (horz.start - vert.start).length <= 1e-8:
|
|
return horz.start
|
|
return horz.end
|
|
# Otherwise they are pointing the same direction
|
|
if (horz.start - vert.end).length < 1e-8:
|
|
return horz.start
|
|
return horz.end
|
|
# If we didn't hit a degenerate case we can treat the segments as infinite lines
|
|
intersect_ratio = (vert.start - horz.start).cross(vert.vector) / denom
|
|
return horz.point_at_ratio(intersect_ratio)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
Envelope().run()
|