621 lines
26 KiB
Python
621 lines
26 KiB
Python
|
#!/usr/bin/env python3
|
||
|
#
|
||
|
# Copyright (C) 2021 Christian Vogt <chris371@topmail-files.de>
|
||
|
#
|
||
|
# recursiveFuseTransform() has originally been written by
|
||
|
# Mark "Klowner" Riedesel
|
||
|
# see: https://github.com/Klowner/inkscape-applytransforms
|
||
|
#
|
||
|
# edgeResize() has been inspired by inkscape-round-corners extension by
|
||
|
# Juergen Weigert <jnweiger@gmail.com>
|
||
|
# see: https://github.com/jnweiger/inkscape-round-corners
|
||
|
#
|
||
|
#
|
||
|
# This program is free software; you can redistribute it and/or modify
|
||
|
# it under the terms of the GNU General Public License version 2, as
|
||
|
# published by the Free Software Foundation.
|
||
|
#
|
||
|
# 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.
|
||
|
#
|
||
|
|
||
|
|
||
|
# This extension was written and tested using Inkscape V1.0.2
|
||
|
# Most probably, it will not work on versions prior to V1.0
|
||
|
#
|
||
|
# Resources I found being useful and inspiring:
|
||
|
# https://gitlab.com/inkscape/extensions/-/wikis/My-First-Effect-Extension
|
||
|
# https://wiki.inkscape.org/wiki/index.php?title=Extension_subsystem
|
||
|
# https://wiki.inkscape.org/wiki/index.php?title=Extensions:_INX_widgets_and_parameters
|
||
|
# https://inkscape.gitlab.io/extensions/documentation/inkex.html
|
||
|
# https://inkscape-extensions-guide.readthedocs.io/en/latest/inkex-modules.html#
|
||
|
# https://gitlab.com/inkscape/extensions
|
||
|
# https://inkscape.gitlab.io/inkscape/doxygen-extensions/index.html
|
||
|
|
||
|
|
||
|
# The purpose of this extension is to help performing parallel
|
||
|
# translations of selected straight paths (lines). This is equivalent
|
||
|
# to changing the absolute X coordinate of a vertical line or changing
|
||
|
# the absolute Y coordinate of a horizontal line, but for lines drawn
|
||
|
# in any angle.
|
||
|
#
|
||
|
# The second purpose is to align a group of objects to such a line
|
||
|
# drawn in any angle. This means: moving and rotating the group and
|
||
|
# adjust its width to match the line length.
|
||
|
#
|
||
|
# To align two objects, it is also possible to turn one of them into
|
||
|
# a group by adding a 'rotation center object' in the middle of the
|
||
|
# selected path/lines, and rotate the group into its 0-degrees position.
|
||
|
# This group can then aligned to the other object.
|
||
|
#
|
||
|
# V0.1 2021-04-20 : initial version.
|
||
|
#
|
||
|
# V0.2 2021-04-21 :
|
||
|
# - Fix: avoid using deprecated inkex API.
|
||
|
# - Fix: move both point and handles of a node to avoid
|
||
|
# non-intentional creating curves out of flat line segments.
|
||
|
# - New: added 'endpoint tolerance' parameter (--endpTol)
|
||
|
# - New: now supports having the rotation center object outside the
|
||
|
# horizontal center of the group.
|
||
|
#
|
||
|
# V0.3 2021-04-28 :
|
||
|
# - New: now allows to select two nodes of a larger path to be used
|
||
|
# as straight line to use for calculating the length and
|
||
|
# translation angle.
|
||
|
# - Mod: Added 'global' section to the information printout.
|
||
|
# - Mod: For closed paths (start point matches end point), we
|
||
|
# use a hardcoded translation angle of 0 degrees.
|
||
|
#
|
||
|
# V0.4 2021-04-29 :
|
||
|
# - New: Added the 'Obj-to-Group' tab.
|
||
|
# - Mod: Updated the info-tab description.
|
||
|
#
|
||
|
# V0.5 2021-05-04 :
|
||
|
# - New: Added 'remove rotation center object' checkbox.
|
||
|
# - Mod: Added LICENCE file.
|
||
|
# - Mod: Changed default rotation center object color to neon green.
|
||
|
# (because pink has already been used for cut-lines in some
|
||
|
# existing drawings)
|
||
|
#
|
||
|
# V0.6 2021-05-05 :
|
||
|
# - Mod: added a workaround for un-selecting a path after we've
|
||
|
# created a rotation-group out of it.
|
||
|
#
|
||
|
# V0.7 2021-05-11 :
|
||
|
# - Mod: Added more error-checking to the 'Obj-to-Group' function.
|
||
|
# - Mod: 'Obj-to-Group' is now able to group multiple objects.
|
||
|
# - Mod: Changed copyright and put version information into inx-file.
|
||
|
#
|
||
|
# V1.0 2021-09-03 :
|
||
|
# - Doc: Moved to github repository.
|
||
|
# - Doc: Added README.md and screenshot art.
|
||
|
# - Doc: Now using the mail address that corresponds to github account.
|
||
|
# - Fix: Fixed stretch/resize option of group alignment.
|
||
|
|
||
|
|
||
|
import math
|
||
|
import inkex
|
||
|
from inkex.paths import CubicSuperPath, Path
|
||
|
from inkex.transforms import Transform
|
||
|
from inkex.styles import Style
|
||
|
|
||
|
NULL_TRANSFORM = Transform([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
|
||
|
|
||
|
|
||
|
class ParallelTranlation(inkex.EffectExtension):
|
||
|
|
||
|
def add_arguments(self, pars):
|
||
|
pars.add_argument("--tab")
|
||
|
# 'translation' Tab
|
||
|
pars.add_argument("--copyMode" , type=str , default="none", help="Move copy or original")
|
||
|
pars.add_argument("--distance" , type=float , default=0 , help="Distance to move")
|
||
|
pars.add_argument("--distUnit" , type=str , default="mm" , help="Unit of the distance")
|
||
|
pars.add_argument("--reverse" , type=inkex.Boolean, default=False , help="Move in opposite direction")
|
||
|
pars.add_argument("--useFixedAngle", type=inkex.Boolean, default=False , help="use fixed translation angle")
|
||
|
# 'align' Tab
|
||
|
pars.add_argument("--fixedAngle" , type=float , default=0 , help="Translation angle")
|
||
|
pars.add_argument("--copyModeA" , type=str , default="none", help="Align copy or original")
|
||
|
pars.add_argument("--lengthModeA" , type=str , default="none", help="Group length adjustment method")
|
||
|
pars.add_argument("--endpTol" , type=int , default=15 , help="Enpoint tolerance (percent of group width)")
|
||
|
pars.add_argument("--colorA" , type=int , default=0 , help="Color of rotation center circle")
|
||
|
pars.add_argument("--rmFromGroup" , type=inkex.Boolean, default=False , help="remove rotation center object from aligned group")
|
||
|
pars.add_argument("--reverseA" , type=inkex.Boolean, default=False , help="additinal group rotate by 180 degrees")
|
||
|
# 'group' Tab
|
||
|
pars.add_argument("--ctSize" , type=float , default=1 , help="Size of rotation center object")
|
||
|
pars.add_argument("--ctSzUnit" , type=str , default="mm" , help="Unit of the size")
|
||
|
pars.add_argument("--reverseG" , type=inkex.Boolean, default=False , help="additinal group rotate by 180 degrees")
|
||
|
|
||
|
|
||
|
def effect(self):
|
||
|
pathCount = 0
|
||
|
pathsWNodesCount = 0
|
||
|
groupCount = 0
|
||
|
for elem in self.svg.selected.values():
|
||
|
if isinstance(elem, inkex.PathElement):
|
||
|
pathCount += 1
|
||
|
nodes = self.count_selected_nodes( elem.get_id() )
|
||
|
if nodes > 0:
|
||
|
if nodes == 2:
|
||
|
pathsWNodesCount += 1
|
||
|
self.groupCenterPath = elem
|
||
|
else:
|
||
|
raise inkex.AbortExtension(
|
||
|
"When selecting individual nodes, you should "
|
||
|
"always select exactly two from the same "
|
||
|
"(sub-)path.")
|
||
|
|
||
|
elif isinstance(elem, inkex.Group):
|
||
|
groupCount += 1
|
||
|
self.alignGroup = elem
|
||
|
|
||
|
if self.options.tab == "align":
|
||
|
if pathCount == 0 or groupCount == 0:
|
||
|
raise inkex.AbortExtension(
|
||
|
"In alignment mode, please select exactly one group "
|
||
|
"and at least one path to align it to.")
|
||
|
if groupCount > 1:
|
||
|
raise inkex.AbortExtension(
|
||
|
"Sorry, we can align a single group only.")
|
||
|
if pathCount > 1 and self.options.copyModeA != 'copy':
|
||
|
raise inkex.AbortExtension(
|
||
|
"To align to multiple paths at once, please choose "
|
||
|
"to apply the alignment to copies of the group.")
|
||
|
elif self.options.tab == "group":
|
||
|
if pathsWNodesCount != 1:
|
||
|
raise inkex.AbortExtension(
|
||
|
"In group mode, please select exactly two nodes "
|
||
|
"from the same (sub-)path.")
|
||
|
else:
|
||
|
if pathCount == 0:
|
||
|
raise inkex.AbortExtension(
|
||
|
"Please select one path at least.")
|
||
|
|
||
|
if self.options.tab == "info":
|
||
|
msg = "Global info:"
|
||
|
self.msg(msg)
|
||
|
msg = " Document name: {}"
|
||
|
self.msg(msg.format(self.svg.name))
|
||
|
msg = " Dimensions: width={} height={} scale={} unit={}"
|
||
|
self.msg(msg.format(self.svg.width, self.svg.height, self.svg.scale, self.svg.unit))
|
||
|
msg = " Selected: {} group(s), {} path(s), {} node(s)"
|
||
|
self.msg(msg.format(groupCount, pathCount, len(self.options.selected_nodes)))
|
||
|
if self.options.selected_nodes:
|
||
|
msg = " Nodes: {}"
|
||
|
self.msg(msg.format(self.options.selected_nodes))
|
||
|
self.msg(' ')
|
||
|
|
||
|
if self.options.tab == "group":
|
||
|
self.process_node(self.groupCenterPath)
|
||
|
else:
|
||
|
for elem in self.svg.selected.values():
|
||
|
self.process_node(elem)
|
||
|
|
||
|
|
||
|
@staticmethod
|
||
|
def close_enough(a, b, maxdist):
|
||
|
# two numbers are compared for close-enough numerical equality
|
||
|
if maxdist <= 0 :
|
||
|
eps = 1e-9
|
||
|
else:
|
||
|
eps = maxdist
|
||
|
return abs(a-b) <= eps
|
||
|
|
||
|
|
||
|
def edgeResize(self, obj, length, rbb):
|
||
|
bbox = obj.bounding_box()
|
||
|
dx = (length - bbox.width) / 2
|
||
|
offcenter = rbb.x.center - bbox.x.center
|
||
|
maxdist = (bbox.width * self.options.endpTol) / 100
|
||
|
|
||
|
# Iterate over all children of the group-object.
|
||
|
# Find path nodes located at the left and right edges of the
|
||
|
# groups bounding box, and move these by 'dx' further to the
|
||
|
# left or right.
|
||
|
for child in obj.iterchildren():
|
||
|
|
||
|
if not isinstance(child, inkex.PathElement):
|
||
|
continue #ignore all non-path-objects
|
||
|
|
||
|
csp = child.path.to_superpath()
|
||
|
node_count = 0;
|
||
|
for sub in csp:
|
||
|
for node in sub:
|
||
|
node_x = node[1][0]
|
||
|
if self.close_enough( node_x, bbox.left, maxdist ):
|
||
|
for i in range(0, 3):
|
||
|
node[i][0] -= (dx - offcenter)
|
||
|
node_count += 1
|
||
|
"""msg = "id:{} touched left edge at:{}"
|
||
|
self.msg(msg.format(child.get_id(),
|
||
|
bbox.left))"""
|
||
|
|
||
|
if self.close_enough( node_x, bbox.right, maxdist ):
|
||
|
for i in range(0, 3):
|
||
|
node[i][0] += (dx + offcenter)
|
||
|
node_count += 1
|
||
|
"""msg = "id:{} touched right edge at:{}"
|
||
|
self.msg(msg.format(child.get_id(),
|
||
|
bbox.right))"""
|
||
|
|
||
|
if node_count > 0:
|
||
|
# we've moved some nodes of this superpath!
|
||
|
# convert back to real path and modify child object in-place
|
||
|
child.set_path(csp.to_path(curves_only=False))
|
||
|
|
||
|
|
||
|
def align(self, x, y, alpha, length):
|
||
|
rotation_col = inkex.Color(self.options.colorA).to_rgba()
|
||
|
rotation_bb = None
|
||
|
|
||
|
# select the object to move
|
||
|
if self.options.copyModeA in ('copy'):
|
||
|
objToMove = self.alignGroup.duplicate()
|
||
|
else:
|
||
|
if self.options.copyModeA in ('obj'):
|
||
|
self.alignGroup.duplicate()
|
||
|
objToMove = self.alignGroup
|
||
|
|
||
|
"""
|
||
|
msg = "align goup {}"
|
||
|
self.msg(msg.format(self.alignGroup.get_id()))
|
||
|
msg = " to: x={} y={}"
|
||
|
self.msg(msg.format(x,y))
|
||
|
msg = " length: {}"
|
||
|
self.msg(msg.format(length))
|
||
|
msg = " angle : {}"
|
||
|
self.msg(msg.format(math.degrees(alpha)))
|
||
|
msg = " rotation center color:{}"
|
||
|
self.msg(msg.format(rotation_col))
|
||
|
for child in self.alignGroup.iterchildren():
|
||
|
bbox = child.bounding_box()
|
||
|
msg = " child-obj:{} fill:{} stroke:{} x={} y={}"
|
||
|
self.msg(msg.format(child.get_id(),
|
||
|
child.style.get_color(name='fill'),
|
||
|
child.style.get_color(name='stroke'),
|
||
|
bbox.x.center,
|
||
|
bbox.y.center))
|
||
|
"""
|
||
|
# first, apply any transformations the object may have already
|
||
|
# so we are starting with no transform assigned to the group
|
||
|
self.recursiveFuseTransform(objToMove)
|
||
|
|
||
|
# locate the rotation center marker in the object to move by
|
||
|
# checking for the rotation marker fill color within the group
|
||
|
for child in objToMove.iterchildren():
|
||
|
if child.style.get_color(name='fill') == rotation_col:
|
||
|
rotation_bb = child.bounding_box()
|
||
|
break
|
||
|
if rotation_bb is None:
|
||
|
raise inkex.AbortExtension(
|
||
|
"No rotation center object found in group.")
|
||
|
|
||
|
# adjust the objects length. Since we haven't moved or rotated
|
||
|
# it by now, we have to adjust its width only.
|
||
|
if self.options.lengthModeA != "none":
|
||
|
if self.options.lengthModeA == "scale":
|
||
|
if not math.isclose( rotation_bb.x.center, objToMove.bounding_box().x.center, rel_tol=1e-05):
|
||
|
msg = "Rotation center x = {}"
|
||
|
self.msg(msg.format(rotation_bb.x.center))
|
||
|
msg = "Group center x = {}"
|
||
|
self.msg(msg.format(objToMove.bounding_box().x.center))
|
||
|
raise inkex.AbortExtension(
|
||
|
"Warning: rotation center is outside the groups horizontal center. "
|
||
|
"This is not supported in stretch/resize adjustment mode. "
|
||
|
"You may want to try the line endpoint adjust mode intead.")
|
||
|
|
||
|
tr = inkex.Transform()
|
||
|
tr.add_scale( length / objToMove.bounding_box().width, 1 )
|
||
|
objToMove.transform = tr * objToMove.transform
|
||
|
self.recursiveFuseTransform(objToMove)
|
||
|
# it looks like the scale transformation also applies
|
||
|
# some horizontal movement, so we need to retrieve
|
||
|
# the rotation_bb again afterwards.
|
||
|
rotation_bb = child.bounding_box()
|
||
|
|
||
|
elif self.options.lengthModeA == "endpoints":
|
||
|
self.edgeResize( objToMove, length, rotation_bb )
|
||
|
|
||
|
# Remove the rotation center object from the group if we have
|
||
|
# been instructed to do so.
|
||
|
if self.options.rmFromGroup:
|
||
|
child.delete()
|
||
|
|
||
|
# then, move it to the desired location
|
||
|
dx = x - rotation_bb.x.center
|
||
|
dy = y - rotation_bb.y.center
|
||
|
tr = inkex.Transform()
|
||
|
tr.add_translate( dx, dy )
|
||
|
objToMove.transform = tr * objToMove.transform
|
||
|
self.recursiveFuseTransform(objToMove)
|
||
|
|
||
|
# finally, rotate it by the given angle at the desired location
|
||
|
tr = inkex.Transform()
|
||
|
tr.add_rotate( math.degrees(alpha), x, y )
|
||
|
objToMove.transform = tr * objToMove.transform
|
||
|
self.recursiveFuseTransform(objToMove)
|
||
|
|
||
|
|
||
|
def make_group(self, parent, x, y, alpha):
|
||
|
# Add a new group and put a rotation center object into it.
|
||
|
group = parent.add(inkex.Group())
|
||
|
style = "color:#000000;fill:{};stroke-width:1;stroke:none".format(
|
||
|
str(inkex.Color(self.options.colorA).to_rgb()))
|
||
|
size = self.svg.unittouu('{}{}'.format(self.options.ctSize,
|
||
|
self.options.ctSzUnit) )
|
||
|
circle = group.add(inkex.Circle(cx=str(x), cy=str(y), r=str(size/2)))
|
||
|
circle.style = inkex.Style().parse_str(style)
|
||
|
|
||
|
# put duplicates of all selected elements into the group.
|
||
|
for elem in self.svg.selected.values():
|
||
|
group.add(elem.duplicate())
|
||
|
|
||
|
# Rotate the whole new group back to it's zero position
|
||
|
tr = inkex.Transform()
|
||
|
tr.add_rotate( math.degrees(-alpha), x, y )
|
||
|
group.transform = tr * group.transform
|
||
|
self.recursiveFuseTransform(group)
|
||
|
|
||
|
# delete all selected elements to remove the selection.
|
||
|
# Note that self.svg.set_selected() don't work. See:
|
||
|
# https://inkscape.org/forums/extensions/how-to-clear-node-selection/
|
||
|
for elem in self.svg.selected.values():
|
||
|
elem.delete()
|
||
|
|
||
|
|
||
|
|
||
|
# Returns the number of selected nodes from the object given
|
||
|
# by name. Indices of selected nodes matching the given object name
|
||
|
# and subpath index are appended to the given index list
|
||
|
def count_selected_nodes(self, name, sub_idx=0, idx_list=[] ):
|
||
|
nodeCount = 0
|
||
|
for node in self.options.selected_nodes:
|
||
|
# The string with selected nodes looks like this:
|
||
|
# 'Object-id:subpath-index:node-index'
|
||
|
# example: 'path1419:0:1'
|
||
|
substr = node.split(":")
|
||
|
if name == substr[0]:
|
||
|
nodeCount += 1
|
||
|
if sub_idx == int(substr[1]):
|
||
|
idx_list.append(int(substr[2]))
|
||
|
return nodeCount
|
||
|
|
||
|
|
||
|
def process_node(self, elem):
|
||
|
if not isinstance(elem, inkex.PathElement):
|
||
|
return # just ignore all non-path elements
|
||
|
|
||
|
sub_idx = 0
|
||
|
hint = ""
|
||
|
for sub in elem.path.to_superpath():
|
||
|
# Calculate the objects rotation angle <alpha>. For this,
|
||
|
# we assume a line between two nodes of the object.
|
||
|
# A horizontal line from left to right is 0 degrees.
|
||
|
# Positive angles means the line is rotated clockwise.
|
||
|
# -180 < alpha <= +180
|
||
|
|
||
|
# Select the two nodes to use for the calculation
|
||
|
node_idx = []
|
||
|
if self.count_selected_nodes( elem.get_id(), sub_idx, node_idx ) > 0:
|
||
|
# we have selected nodes from this element.
|
||
|
if len(node_idx) == 2:
|
||
|
# if the user selected exactly two individual nodes
|
||
|
# of a probably larger path, we shall use these.
|
||
|
p1_idx = node_idx[0]
|
||
|
p2_idx = node_idx[1]
|
||
|
hint = " (Segment defined by selected nodes)"
|
||
|
else:
|
||
|
# otherwise, we'll skip this sub-path and try the
|
||
|
# next from the same element.
|
||
|
sub_idx += 1
|
||
|
continue
|
||
|
else:
|
||
|
# if no nodes have been selected in this element, we
|
||
|
# use the start- and end-node of the sub-path
|
||
|
p1_idx = 0
|
||
|
p2_idx = -1
|
||
|
|
||
|
# Calculate angle, length, midpoint, etc.
|
||
|
x1=sub[p1_idx][1][0]
|
||
|
y1=sub[p1_idx][1][1]
|
||
|
x2=sub[p2_idx][1][0]
|
||
|
y2=sub[p2_idx][1][1]
|
||
|
width = x2-x1
|
||
|
heigth= y2-y1
|
||
|
if math.isclose( width, 0 ):
|
||
|
if math.isclose( heigth, 0 ):
|
||
|
alpha = 0
|
||
|
elif heigth > 0:
|
||
|
alpha = math.pi/2
|
||
|
else:
|
||
|
alpha = -math.pi/2
|
||
|
else:
|
||
|
alpha = math.atan(heigth/width)
|
||
|
if width < 0:
|
||
|
if heigth < 0:
|
||
|
alpha -= math.pi
|
||
|
else:
|
||
|
alpha += math.pi
|
||
|
xm = (x1+x2)/2
|
||
|
ym = (y1+y2)/2
|
||
|
length = math.sqrt( math.pow(width,2) + math.pow(heigth,2) )
|
||
|
|
||
|
# Select translation angle by options
|
||
|
if self.options.useFixedAngle:
|
||
|
da = math.radians(self.options.fixedAngle)
|
||
|
else:
|
||
|
da = alpha
|
||
|
|
||
|
# Calculate translation in x and y direction
|
||
|
# A positive distance value moves towards greater coordinates
|
||
|
dist = self.svg.unittouu('{}{}'.format(self.options.distance,
|
||
|
self.options.distUnit) )
|
||
|
if self.options.reverse:
|
||
|
da = da + math.radians(180)
|
||
|
dx = -math.sin(da) * dist
|
||
|
dy = math.cos(da) * dist
|
||
|
|
||
|
if self.options.tab == "translation":
|
||
|
if self.options.copyMode in ('copy'):
|
||
|
objToMove = elem.duplicate()
|
||
|
else:
|
||
|
if self.options.copyMode in ('obj'):
|
||
|
elem.duplicate()
|
||
|
objToMove = elem
|
||
|
objToMove.path = objToMove.path.translate( dx, dy )
|
||
|
|
||
|
elif self.options.tab == "align":
|
||
|
if self.options.reverseA:
|
||
|
alpha = alpha + math.radians(180)
|
||
|
self.align(xm, ym, alpha, length)
|
||
|
|
||
|
elif self.options.tab == "group":
|
||
|
if length > 0:
|
||
|
if self.options.reverseG:
|
||
|
alpha = alpha + math.radians(180)
|
||
|
self.make_group(elem.getparent(), xm, ym, alpha)
|
||
|
|
||
|
elif self.options.tab == "info":
|
||
|
msg = "Measuring result:"
|
||
|
self.msg(msg)
|
||
|
msg = " object name: {}"
|
||
|
self.msg(msg.format(elem.get_id()))
|
||
|
msg = " subpath: {} {}"
|
||
|
self.msg(msg.format(sub_idx, hint))
|
||
|
msg = " start point: x={} y={}"
|
||
|
self.msg(msg.format(x1,y1))
|
||
|
msg = " end point: x={} y={}"
|
||
|
self.msg(msg.format(x2,y2))
|
||
|
msg = " mid point: x={} y={}"
|
||
|
self.msg(msg.format(xm,ym))
|
||
|
msg = " object length: {}"
|
||
|
self.msg(msg.format(length))
|
||
|
msg = " object angle : {}°"
|
||
|
self.msg(msg.format(math.degrees(alpha)))
|
||
|
msg = " translation angle: {}°"
|
||
|
self.msg(msg.format(math.degrees(da)))
|
||
|
msg = " translation: dx={} dy={}"
|
||
|
self.msg(msg.format(dx,dy))
|
||
|
self.msg(" ")
|
||
|
|
||
|
# continue for-loop with next sub-index
|
||
|
sub_idx += 1
|
||
|
|
||
|
|
||
|
@staticmethod
|
||
|
def objectToPath(node):
|
||
|
if node.tag == inkex.addNS('g', 'svg'):
|
||
|
return node
|
||
|
|
||
|
if node.tag == inkex.addNS('path', 'svg') or node.tag == 'path':
|
||
|
for attName in node.attrib.keys():
|
||
|
if ("sodipodi" in attName) or ("inkscape" in attName):
|
||
|
del node.attrib[attName]
|
||
|
return node
|
||
|
|
||
|
return node
|
||
|
|
||
|
|
||
|
def recursiveFuseTransform(self, node, transf=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
|
||
|
|
||
|
transf = Transform(transf) * Transform(node.get("transform", None))
|
||
|
|
||
|
if 'transform' in node.attrib:
|
||
|
del node.attrib['transform']
|
||
|
|
||
|
node = self.objectToPath(node)
|
||
|
|
||
|
if transf == NULL_TRANSFORM:
|
||
|
# Don't do anything if there is effectively no transform applied
|
||
|
# reduces alerts for unsupported nodes
|
||
|
pass
|
||
|
elif 'd' in node.attrib:
|
||
|
d = node.get('d')
|
||
|
p = CubicSuperPath(d)
|
||
|
p = Path(p).to_absolute().transform(transf, True)
|
||
|
node.set('d', str(Path(CubicSuperPath(p).to_path())))
|
||
|
|
||
|
elif node.tag in [inkex.addNS('polygon', 'svg'),
|
||
|
inkex.addNS('polyline', 'svg')]:
|
||
|
points = node.get('points')
|
||
|
points = points.strip().split(' ')
|
||
|
for k, p in enumerate(points):
|
||
|
if ',' in p:
|
||
|
p = p.split(',')
|
||
|
p = [float(p[0]), float(p[1])]
|
||
|
p = transf.apply_to_point(p)
|
||
|
p = [str(p[0]), str(p[1])]
|
||
|
p = ','.join(p)
|
||
|
points[k] = p
|
||
|
points = ' '.join(points)
|
||
|
node.set('points', points)
|
||
|
|
||
|
elif node.tag in [inkex.addNS("ellipse", "svg"), inkex.addNS("circle", "svg")]:
|
||
|
|
||
|
def isequal(a, b):
|
||
|
return abs(a - b) <= transf.absolute_tolerance
|
||
|
|
||
|
if node.TAG == "ellipse":
|
||
|
rx = float(node.get("rx"))
|
||
|
ry = float(node.get("ry"))
|
||
|
else:
|
||
|
rx = float(node.get("r"))
|
||
|
ry = rx
|
||
|
|
||
|
cx = float(node.get("cx"))
|
||
|
cy = float(node.get("cy"))
|
||
|
sqxy1 = (cx - rx, cy - ry)
|
||
|
sqxy2 = (cx + rx, cy - ry)
|
||
|
sqxy3 = (cx + rx, cy + ry)
|
||
|
newxy1 = transf.apply_to_point(sqxy1)
|
||
|
newxy2 = transf.apply_to_point(sqxy2)
|
||
|
newxy3 = transf.apply_to_point(sqxy3)
|
||
|
|
||
|
node.set("cx", (newxy1[0] + newxy3[0]) / 2)
|
||
|
node.set("cy", (newxy1[1] + newxy3[1]) / 2)
|
||
|
edgex = math.sqrt(
|
||
|
abs(newxy1[0] - newxy2[0]) ** 2 + abs(newxy1[1] - newxy2[1]) ** 2
|
||
|
)
|
||
|
edgey = math.sqrt(
|
||
|
abs(newxy2[0] - newxy3[0]) ** 2 + abs(newxy2[1] - newxy3[1]) ** 2
|
||
|
)
|
||
|
|
||
|
"""
|
||
|
if not isequal(edgex, edgey) and (
|
||
|
node.TAG == "circle"
|
||
|
or not isequal(newxy2[0], newxy3[0])
|
||
|
or not isequal(newxy1[1], newxy2[1])
|
||
|
):
|
||
|
inkex.utils.errormsg(
|
||
|
"Warning: Shape %s (%s) is approximate only, try Object to path first for better results"
|
||
|
% (node.TAG, node.get("id"))
|
||
|
)
|
||
|
"""
|
||
|
|
||
|
if node.TAG == "ellipse":
|
||
|
node.set("rx", edgex / 2)
|
||
|
node.set("ry", edgey / 2)
|
||
|
else:
|
||
|
node.set("r", edgex / 2)
|
||
|
|
||
|
elif node.tag in [inkex.addNS('rect', 'svg'),
|
||
|
inkex.addNS('text', 'svg'),
|
||
|
inkex.addNS('image', 'svg'),
|
||
|
inkex.addNS('use', 'svg')]:
|
||
|
inkex.utils.errormsg(
|
||
|
"Shape %s (%s) not yet supported, try Object to path first"
|
||
|
% (node.TAG, node.get("id"))
|
||
|
)
|
||
|
|
||
|
for child in node.getchildren():
|
||
|
self.recursiveFuseTransform(child, transf)
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
ParallelTranlation().run()
|