This repository has been archived on 2023-03-25. You can view files and clone it, but cannot push or open issues or pull requests.
mightyscape-1.1-deprecated/extensions/fablabchemnitz/reorder_sequence/reorder_sequence.py

1240 lines
56 KiB
Python
Raw Normal View History

# coding=utf-8
#
# SVG Path Ordering Extension
# This extension uses a simple TSP algorithm to order the paths so as
# to reduce plotting time by plotting nearby paths consecutively.
#
#
# While written from scratch, this is a derivative in spirit of the work by
# Matthew Beckler and Daniel C. Newman for the EggBot project.
#
# The MIT License (MIT)
#
# Copyright (c) 2020 Windell H. Oskay, Evil Mad Science LLC
# www.evilmadscientist.com
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import math
import sys
from lxml import etree
import inkex
import simpletransform
import simplestyle
import plot_utils
"""
TODOs:
* Apparent difference in execution time for portrait vs landscape document orientation.
Seems to be related to the _change_
* Implement path functions
<param name="path_handling" _gui-text="Compound Paths" type="optiongroup">
<_option value=0>Leave as is</_option>
<_option value=1>Reorder subpaths</_option>
<_option value=2>Break apart</_option>
</param>
self.OptionParser.add_option( "--path_handling",\
action="store", type="int", dest="path_handling",\
default=1,help="How compound paths are handled")
* Consider re-introducing GUI method for rendering:
<param indent="1" name="rendering" type="boolean" _gui-text="Preview pen-up travel">false</param>
"""
2021-04-16 14:41:12 +02:00
class ReorderEffect(inkex.EffectExtension):
"""
Inkscape effect extension.
Re-order the objects in the SVG document for faster plotting.
Respect layers: Initialize a new dictionary of objects for each layer, and sort
objects within that layer only
Objects in root of document are treated as being on a _single_ layer, and will all
be sorted.
"""
2021-04-16 14:41:12 +02:00
def add_arguments(self, pars):
pars.add_argument( "--reordering",type=int, default=1, help="How groups are handled")
pars.add_argument( "--preview_rendering",type=inkex.Boolean, default=False, help="Preview rendering") # Rendering is available for debug purposes. It only previews pen-up movements that are reordered and typically does not include all possible movement.
self.auto_rotate = True
def effect(self):
# Main entry point of the program
self.svg_width = 0
self.svg_height = 0
self.air_total_default = 0
self.air_total_sorted = 0
self.printPortrait = False
self.layer_index = 0 # index for coloring layers
self.svg = self.document.getroot()
self.DocUnits = "in" # Default
self.DocUnits = self.svg.unit
self.unit_scaling = 1
self.getDocProps()
"""
Set up the document-wide transforms to handle SVG viewbox
"""
matCurrent = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
viewbox = self.svg.get( 'viewBox' )
vb = self.svg.get('viewBox')
if vb:
p_a_r = self.svg.get('preserveAspectRatio')
sx,sy,ox,oy = plot_utils.vb_scale(vb, p_a_r, self.svg_width, self.svg_height)
else:
sx = 1.0 / float(plot_utils.PX_PER_INCH) # Handle case of no viewbox
sy = sx
ox = 0.0
oy = 0.0
# Initial transform of document is based on viewbox, if present:
matCurrent = simpletransform.parseTransform('scale({0:.6E},{1:.6E}) translate({2:.6E},{3:.6E})'.format(sx, sy, ox, oy))
# Set up x_last, y_last, which keep track of last known pen position
# The initial position is given by the expected initial pen position
self.y_last = 0
if (self.printPortrait):
self.x_last = self.svg_width
else:
self.x_last = 0
parent_vis='visible'
self.root_nodes = []
if self.options.preview_rendering == True:
# Remove old preview layers, if rendering is enabled
for node in self.svg:
if node.tag == inkex.addNS( 'g', 'svg' ) or node.tag == 'g':
if ( node.get( inkex.addNS( 'groupmode', 'inkscape' ) ) == 'layer' ):
LayerName = node.get( inkex.addNS( 'label', 'inkscape' ) )
if LayerName == '% Preview':
self.svg.remove( node )
preview_transform = simpletransform.parseTransform(
'translate({2:.6E},{3:.6E}) scale({0:.6E},{1:.6E})'.format(
1.0/sx, 1.0/sy, -ox, -oy))
path_attrs = { 'transform': simpletransform.formatTransform(preview_transform)}
self.preview_layer = etree.Element(inkex.addNS('g', 'svg'),
path_attrs, nsmap=inkex.NSS)
self.preview_layer.set( inkex.addNS('groupmode', 'inkscape' ), 'layer' )
self.preview_layer.set( inkex.addNS( 'label', 'inkscape' ), '% Preview' )
self.svg.append( self.preview_layer )
# Preview stroke width: 1/1000 of page width or height, whichever is smaller
if self.svg_width < self.svg_height:
width_du = self.svg_width / 1000.0
else:
width_du = self.svg_height / 1000.0
"""
Stroke-width is a css style element, and cannot accept scientific notation.
Thus, in cases with large scaling (i.e., high values of 1/sx, 1/sy)
resulting from the viewbox attribute of the SVG document, it may be necessary to use
a _very small_ stroke width, so that the stroke width displayed on the screen
has a reasonable width after being displayed greatly magnified thanks to the viewbox.
Use log10(the number) to determine the scale, and thus the precision needed.
"""
log_ten = math.log10(width_du)
if log_ten > 0: # For width_du > 1
width_string = "{0:.3f}".format(width_du)
else:
prec = int(math.ceil(-log_ten) + 3)
width_string = "{0:.{1}f}".format(width_du, prec)
self.p_style = {'stroke-width': width_string, 'fill': 'none',
'stroke-linejoin': 'round', 'stroke-linecap': 'round'}
self.svg = self.parse_svg(self.svg, matCurrent)
def parse_svg(self, input_node, mat_current=None, parent_vis='visible'):
"""
Input: An SVG node (usually) containing other nodes:
The SVG root, a layer, sublayer, or other group.
Output: The re-ordered node. The contents are reordered with the greedy
algorithm, except:
- Layers and sublayers are preserved. The contents of each are
re-ordered for faster plotting.
- Groups are either preserved, broken apart, or re-ordered within
the group, depending on the value of group_mode.
"""
coord_dict = {}
# coord_dict maps a node ID to the following data:
# Is the node plottable, first coordinate pair, last coordinate pair.
# i.e., Node_id -> (Boolean: plottable, Xi, Yi, Xf, Yf)
group_dict = {}
# group_dict maps a node ID for a group to the contents of that group.
# The contents may be a preserved nested group or a flat list, depending
# on the selected group handling mode. Example:
# group_dict = {'id_1': <Element {http://www.w3.org/2000/svg}g at memory_location_1>,
# 'id_2': <Element {http://www.w3.org/2000/svg}g at memory_location_2>
nodes_to_delete = []
counter = 0 # TODO: Replace this with better unique ID system
# Account for input_node's transform and any transforms above it:
if mat_current is None:
mat_current = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
try:
matNew = simpletransform.composeTransform( mat_current,
simpletransform.parseTransform( input_node.get( "transform" )))
except AttributeError:
matNew = mat_current
for node in input_node:
# Step through each object within the top-level input node
if node.tag is etree.Comment:
continue
try:
id = node.get( 'id' )
except AttributeError:
id = self.svg.get_unique_id("1",True)
node.set( 'id', id)
if id == None:
id = self.svg.get_unique_id("1",True)
node.set( 'id', id)
# First check for object visibility:
skip_object = False
# Check for "display:none" in the node's style attribute:
style = dict(inkex.Style.parse_str(node.get('style')))
if 'display' in style.keys() and style['display'] == 'none':
skip_object = True # Plot neither this object nor its children
# The node may have a display="none" attribute as well:
if node.get( 'display' ) == 'none':
skip_object = True # Plot neither this object nor its children
# Visibility attributes control whether a given object will plot.
# Children of hidden (not visible) parents may be plotted if
# they assert visibility.
visibility = node.get( 'visibility', parent_vis )
if 'visibility' in style.keys():
visibility = style['visibility'] # Style may override attribute.
if visibility == 'inherit':
visibility = parent_vis
if visibility != 'visible':
skip_object = True # Skip this object and its children
# Next, check to see if this inner node is itself a group or layer:
if node.tag == inkex.addNS( 'g', 'svg' ) or node.tag == 'g':
# Use the user-given option to decide what to do with subgroups:
subgroup_mode = self.options.reordering
# Values of the parameter:
# subgroup_mode=="1": Preserve groups
# subgroup_mode=="2": Reorder within groups
# subgroup_mode=="3": Break apart groups
if node.get(inkex.addNS('groupmode', 'inkscape')) == 'layer':
# The node is a layer or sub-layer, not a regular group.
# Parse it separately, and re-order its contents.
subgroup_mode = 2 # Always sort within each layer.
self.layer_index += 1
layer_name = node.get( inkex.addNS( 'label', 'inkscape' ) )
if sys.version_info < (3,): # Yes this is ugly. More elegant suggestions welcome. :)
layer_name = layer_name.encode( 'ascii', 'ignore' ) #Drop non-ascii characters
else:
layer_name = str(layer_name)
layer_name.lstrip # Remove leading whitespace
if layer_name:
if layer_name[0] == '%': # First character is '%'; This
skip_object = True # is a documentation layer; skip plotting.
self.layer_index -= 1 # Set this back to previous value.
if skip_object:
# Do not re-order hidden groups or layers.
subgroup_mode = 1 # Preserve this group
if subgroup_mode == 3:
# Break apart this non-layer subgroup and add it to
# the set of things to be re-ordered.
nodes_to_delete.append(node)
nodes_inside_group = self.group2NodeDict(node)
for a_node in nodes_inside_group:
try:
id = a_node.get( 'id' )
except AttributeError:
id = self.uniqueId("1",True)
a_node.set( 'id', id)
if id == None:
id = self.uniqueId("1",True)
a_node.set( 'id', id)
# Use getFirstPoint and getLastPoint on each object:
start_plottable, first_point = self.getFirstPoint(a_node, matNew)
end_plottable, last_point = self.getLastPoint(a_node, matNew)
coord_dict[id] = (start_plottable and end_plottable,
first_point[0], first_point[1], last_point[0], last_point[1] )
# Entry in group_dict is this node
group_dict[id] = a_node
elif subgroup_mode == 2:
# Reorder a layer or subgroup with a recursive call.
node = self.parse_svg(node, matNew, visibility)
# Capture the first and last x,y coordinates of the optimized node
start_plottable, first_point = self.group_first_pt(node, matNew)
end_plottable, last_point = self.group_last_pt(node, matNew)
# Then add this optimized node to the coord_dict
coord_dict[id] = (start_plottable and end_plottable,
first_point[0], first_point[1], last_point[0], last_point[1] )
# Entry in group_dict is this node
group_dict[id] = node
else: # (subgroup_mode == 1)
# Preserve the group, but find its first and last point so
# that it can be re-ordered with respect to other items
if skip_object:
start_plottable = False
end_plottable = False
first_point = [(-1.), (-1.)]
last_point = [(-1.), (-1.)]
else:
start_plottable, first_point = self.group_first_pt(node, matNew)
end_plottable, last_point = self.group_last_pt(node, matNew)
coord_dict[id] = (start_plottable and end_plottable,
first_point[0], first_point[1], last_point[0], last_point[1] )
# Entry in group_dict is this node
group_dict[id] = node
else: # Handle objects that are not groups
if skip_object:
start_plottable = False
end_plottable = False
first_point = [(-1.), (-1.)]
last_point = [(-1.), (-1.)]
else:
start_plottable, first_point = self.getFirstPoint(node, matNew)
end_plottable, last_point = self.getLastPoint(node, matNew)
coord_dict[id] = (start_plottable and end_plottable,
first_point[0], first_point[1], last_point[0], last_point[1] )
group_dict[id] = node # Entry in group_dict is this node
# Perform the re-ordering:
ordered_element_list = self.ReorderNodeList(coord_dict, group_dict)
# Once a better order for the svg elements has been determined,
# All there is do to is to reintroduce the nodes to the parent in the correct order
for elt in ordered_element_list:
# Creates identical node at the correct location according to ordered_element_list
input_node.append(elt)
# Once program is finished parsing through
for element_to_remove in nodes_to_delete:
try:
input_node.remove(element_to_remove)
except ValueError:
inkex.errormsg(str(element_to_remove.get('id'))+" is not a member of " + str(input_node.get('id')))
return input_node
def break_apart_path(self, path):
"""
An SVG path may contain multiple distinct portions, that are normally separated
by pen-up movements.
This function takes the path data string from an SVG path, parses it, and returns
a dictionary of independent path data strings, each of which represents a single
pen-down movement. It is equivalent to the Inkscape function Path > Break Apart
Input: path data string, representing a single SVG path
Output: Dictionary of (separated) path data strings
"""
MaxLength = len(path)
ix = 0
move_to_location = []
path_dictionary = {}
path_list = []
path_number = 1
# Search for M or m location
while ix < MaxLength:
if(path[ix] == 'm' or path[ix] == 'M'):
move_to_location.append(ix)
ix = ix + 1
# Iterate through every M or m location in our list of move to instructions
# Slice the path string according to path beginning and ends as indicated by the
# location of these instructions
for counter, m in enumerate(move_to_location):
if (m == move_to_location[-1]):
# last entry
path_list.append(path[m:MaxLength].rstrip())
else:
path_list.append(path[m:move_to_location[counter + 1]].rstrip())
for counter, current_path in enumerate(path_list):
# Enumerate over every entry in the path looking for relative m commands
if current_path[0] == 'm' and counter > 0:
# If path contains relative m command, the best case is when the last command
# was a Z or z. In this case, all relative operations are performed relative to
# initial x, y coordinates of the previous path
if path_list[counter -1][-1].upper() == 'Z':
current_path_x, current_path_y,index = self.getFirstPoint(current_path, matNew)
prev_path_x, prev_path_y,ignore = self.getFirstPoint(path_list[counter-1])
adapted_x = current_path_x + prev_path_x
adapted_y = current_path_y + prev_path_y
# Now we can replace the path data with an Absolute Move to instruction
# HOWEVER, we need to adapt all the data until we reach a different command in the case of a repeating
path_list[counter] = "m "+str(adapted_x)+","+str(adapted_y) + ' ' +current_path[index:]
# If there is no z or absolute commands, we need to parse the entire path
else:
# scan path for absolute coordinates. If present, begin parsing from their index
# instead of the beginning
prev_path = path_list[counter-1]
prev_path_length = len(prev_path)
jx = 0
x_val, y_val = 0,0
# Check one char at a time
# until we have the moveTo Command
last_command = ''
is_absolute_command = False
repeated_command = False
# name of command
# how many parameters we need to skip
accepted_commands = {
'M':0,
'L':0,
'H':0,
'V':0,
'C':4,
'S':2,
'Q':2,
'T':0,
'A':5
}
# If there is an absolute command which specifies a new initial point
# then we can save time by setting our index directly to its location in the path data
# See if an accepted_command is present in the path data. If it is present further in the
# string than any command found before, then set the pointer to that location
# if a command is not found, find() will return a -1. jx is initialized to 0, so if no matches
# are found, the program will parse from the beginning to the end of the path
for keys in 'MLCSQTA': # TODO: Compare to last_point; see if we can clean up this part
if(prev_path.find(keys) > jx):
jx = prev_path.find(keys)
while jx < prev_path_length:
temp_x_val = ''
temp_y_val = ''
num_of_params_to_skip = 0
# SVG Path commands can be repeated
if (prev_path[jx].isdigit() and last_command):
repeated_command = True
else:
repeated_command = False
if (prev_path[jx].isalpha() and prev_path[jx].upper() in accepted_commands) or repeated_command:
if repeated_command:
#is_relative_command is saved from last iteration of the loop
current_command = last_command
else:
# If the character is accepted, we must parse until reach the x y coordinates
is_absolute_command = prev_path[jx].isupper()
current_command = prev_path[jx].upper()
# Each command has a certain number of parameters we must pass before we reach the
# information we care about. We will parse until we know that we have reached them
# Get to start of next number
# We will know we have reached a number if the current character is a +/- sign
# or current character is a digit
while jx < prev_path_length:
if(prev_path[jx] in '+-' or prev_path[jx].isdigit()):
break
jx = jx + 1
# We need to parse past the unused parameters in our command
# The number of parameters to parse past is dependent on the command and stored
# as the value of accepted_command
# Spaces and commas are used to deliniate paramters
while jx < prev_path_length and num_of_params_to_skip < accepted_commands[current_command]:
if(prev_path[jx].isspace() or prev_path[jx] == ','):
num_of_params_to_skip = num_of_params_to_skip + 1
jx = jx + 1
# Now, we are in front of the x character
if current_command.upper() == 'V':
temp_x_val = 0
if current_command.upper() == 'H':
temp_y_val = 0
# Parse until next character is a digit or +/- character
while jx < prev_path_length and current_command.upper() != 'V':
if(prev_path[jx] in '+-' or prev_path[jx].isdigit()):
break
jx = jx + 1
# Save each next character until we reach a space
while jx < prev_path_length and current_command.upper() != 'V' and not (prev_path[jx].isspace() or prev_path[jx] == ','):
temp_x_val = temp_x_val + prev_path[jx]
jx = jx + 1
# Then we know we have completely parsed the x character
# Now we are in front of the y character
# Parse until next character is a digit or +/- character
while jx < prev_path_length and current_command.upper() != 'H':
if(prev_path[jx] in '+-' or prev_path[jx].isdigit()):
break
jx = jx + 1
## Save each next character until we reach a space
while jx < prev_path_length and current_command.upper() != 'H' and not (prev_path[jx].isspace() or prev_path[jx] == ','):
temp_y_val = temp_y_val + prev_path[jx]
jx = jx + 1
# Then we know we have completely parsed the y character
if is_absolute_command:
if current_command == 'H':
# Absolute commands create new x,y position
try:
x_val = float(temp_x_val)
except ValueError:
pass
elif current_command == 'V':
# Absolute commands create new x,y position
try:
y_val = float(temp_y_val)
except ValueError:
pass
else:
# Absolute commands create new x,y position
try:
x_val = float(temp_x_val)
y_val = float(temp_y_val)
except ValueError:
pass
else:
if current_command == 'h':
# Absolute commands create new x,y position
try:
x_val = x_val + float(temp_x_val)
except ValueError:
pass
elif current_command == 'V':
# Absolute commands create new x,y position
try:
y_val = y_val + float(temp_y_val)
except ValueError:
pass
else:
# Absolute commands create new x,y position
try:
x_val = x_val + float(temp_x_val)
y_val = y_val + float(temp_y_val)
except ValueError:
pass
last_command = current_command
jx = jx + 1
x,y,index = self.getFirstPoint(current_path,None)
path_list[counter] = "m "+str(x_val+x)+","+str(y_val+y) + ' ' + current_path[index:]
for counter, path in enumerate(path_list):
path_dictionary['ad_path'+ str(counter)] = path
return path_dictionary
def getFirstPoint(self, node, matCurrent):
"""
Input: (non-group) node and parent transformation matrix
Output: Boolean value to indicate if the svg element is plottable and
two floats stored in a list representing the x and y coordinates we plot first
"""
# first apply the current matrix transform to this node's transform
matNew = simpletransform.composeTransform( matCurrent, simpletransform.parseTransform( node.get( "transform" ) ) )
point = [float(-1), float(-1)]
try:
if node.tag == inkex.addNS( 'path', 'svg' ):
pathdata = node.get('d')
point = plot_utils.pathdata_first_point(pathdata)
simpletransform.applyTransformToPoint(matNew, point)
return True, point
if node.tag == inkex.addNS( 'rect', 'svg' ) or node.tag == 'rect':
"""
The x,y coordinates for a rect are included in their specific attributes
If there is a transform, we need translate the x & y coordinates to their
correct location via applyTransformToPoint.
"""
point[0] = float( node.get( 'x' ) )
point[1] = float( node.get( 'y' ) )
simpletransform.applyTransformToPoint(matNew, point)
return True, point
if node.tag == inkex.addNS( 'line', 'svg' ) or node.tag == 'line':
"""
The x1 and y1 attributes are where we will start to draw
So, get them, apply the transform matrix, and return the point
"""
point[0] = float( node.get( 'x1' ) )
point[1] = float( node.get( 'y1' ) )
simpletransform.applyTransformToPoint(matNew, point)
return True, point
if node.tag == inkex.addNS( 'polyline', 'svg' ) or node.tag == 'polyline':
pl = node.get( 'points', '' ).strip()
if pl == '':
return False, point
pa = pl.replace(',',' ').split() # replace comma with space before splitting
if not pa:
return False, point
pathLength = len( pa )
if (pathLength < 4): # Minimum of x1,y1 x2,y2 required.
return False, point
d = "M " + pa[0] + " " + pa[1]
i = 2
while (i < (pathLength - 1 )):
d += " L " + pa[i] + " " + pa[i + 1]
i += 2
point = plot_utils.pathdata_first_point(d)
simpletransform.applyTransformToPoint(matNew, point)
return True, point
if (node.tag == inkex.addNS( 'polygon', 'svg' ) or
node.tag == 'polygon'):
"""
We need to extract x1 and y1 from these:
<polygon points="x1,y1 x2,y2 x3,y3 [...]"/>
We accomplish this with Python string strip
and split methods. Then apply transforms
"""
# Strip() removes all whitespace from the start and end of p1
pl = node.get( 'points', '' ).strip()
if (pl == ''):
# If pl is blank there has been an error, return False and -1,-1 to indicate a problem has occured
return False, point
# Split string by whitespace
pa = pl.split()
if not len( pa ):
# If pa is blank there has been an error, return False and -1,-1 to indicate a problem has occured
return False, point
# pa[0] = "x1,y1
# split string via comma to get x1 and y1 individually
# then point = [x1,x2]
point = pa[0].split(",")
point = [float(point[0]),float(point[1])]
simpletransform.applyTransformToPoint(matNew, point)
return True, point
if node.tag == inkex.addNS( 'ellipse', 'svg' ) or \
node.tag == 'ellipse':
cx = float( node.get( 'cx', '0' ) )
cy = float( node.get( 'cy', '0' ) )
rx = float( node.get( 'rx', '0' ) )
point[0] = cx - rx
point[1] = cy
simpletransform.applyTransformToPoint(matNew, point)
return True, point
if node.tag == inkex.addNS( 'circle', 'svg' ) or \
node.tag == 'circle':
cx = float( node.get( 'cx', '0' ) )
cy = float( node.get( 'cy', '0' ) )
r = float( node.get( 'r', '0' ) )
point[0] = cx - r
point[1] = cy
simpletransform.applyTransformToPoint(matNew, point)
return True, point
if node.tag == inkex.addNS('symbol', 'svg') or node.tag == 'symbol':
# A symbol is much like a group, except that
# it's an invisible object.
return False, point # Skip this element.
if node.tag == inkex.addNS('use', 'svg') or node.tag == 'use':
"""
A <use> element refers to another SVG element via an xlink:href="#blah"
attribute. We will handle the element by doing an XPath search through
the document, looking for the element with the matching id="blah"
attribute. We then recursively process that element after applying
any necessary (x,y) translation.
Notes:
1. We ignore the height and g attributes as they do not apply to
path-like elements, and
2. Even if the use element has visibility="hidden", SVG still calls
for processing the referenced element. The referenced element is
hidden only if its visibility is "inherit" or "hidden".
3. We may be able to unlink clones using the code in pathmodifier.py
"""
refid = node.get(inkex.addNS('href', 'xlink'))
if refid is not None:
# [1:] to ignore leading '#' in reference
path = '//*[@id="{0}"]'.format(refid[1:])
refnode = node.xpath(path)
if refnode is not None:
x = float(node.get('x', '0'))
y = float(node.get('y', '0'))
# Note: the transform has already been applied
if x != 0 or y != 0:
mat_new2 = simpletransform.composeTransform(matNew, simpletransform.parseTransform('translate({0:f},{1:f})'.format(x, y)))
else:
mat_new2 = matNew
# Note that the referenced object may be a 'symbol`,
# which acts like a group, or it may be a simple
# object.
if len(refnode) > 0:
plottable, the_point = self.group_first_pt(refnode[0], mat_new2)
else:
plottable, the_point = self.group_first_pt(refnode, mat_new2)
return plottable, the_point
except:
pass
# Svg Object is not a plottable element
# In this case, return False to indicate a non-plottable element
# and a default point
return False, point
def getLastPoint(self, node, matCurrent):
"""
Input: XML tree node and transformation matrix
Output: Boolean value to indicate if the svg element is plottable or not and
two floats stored in a list representing the x and y coordinates we plot last
"""
# first apply the current matrix transform to this node's transform
matNew = simpletransform.composeTransform( matCurrent, simpletransform.parseTransform( node.get( "transform" ) ) )
# If we return a negative value, we know that this function did not work
point = [float(-1), float(-1)]
try:
if node.tag == inkex.addNS( 'path', 'svg' ):
path = node.get('d')
point = plot_utils.pathdata_last_point(path)
simpletransform.applyTransformToPoint(matNew, point)
return True, point
if node.tag == inkex.addNS( 'rect', 'svg' ) or node.tag == 'rect':
"""
The x,y coordinates for a rect are included in their specific attributes
If there is a transform, we need translate the x & y coordinates to their
correct location via applyTransformToPoint.
"""
point[0] = float( node.get( 'x' ) )
point[1] = float( node.get( 'y' ) )
simpletransform.applyTransformToPoint(matNew, point)
return True, point # Same start and end points
if node.tag == inkex.addNS( 'line', 'svg' ) or node.tag == 'line':
"""
The x2 and y2 attributes are where we will end our drawing
So, get them, apply the transform matrix, and return the point
"""
point[0] = float( node.get( 'x2' ) )
point[1] = float( node.get( 'y2' ) )
simpletransform.applyTransformToPoint(matNew, point)
return True, point
if node.tag == inkex.addNS( 'polyline', 'svg' ) or node.tag == 'polyline':
pl = node.get( 'points', '' ).strip()
if pl == '':
return False, point
pa = pl.replace(',',' ').split()
if not pa:
return False, point
pathLength = len( pa )
if (pathLength < 4): # Minimum of x1,y1 x2,y2 required.
return False, point
d = "M " + pa[0] + " " + pa[1]
i = 2
while (i < (pathLength - 1 )):
d += " L " + pa[i] + " " + pa[i + 1]
i += 2
endpoint = plot_utils.pathdata_last_point(d)
simpletransform.applyTransformToPoint(matNew, endpoint)
return True, endpoint
if node.tag == inkex.addNS( 'polygon', 'svg' ) or node.tag == 'polygon':
"""
We need to extract x1 and y1 from these:
<polygon points="x1,y1 x2,y2 x3,y3 [...]"/>
We accomplish this with Python string strip
and split methods. Then apply transforms
"""
# Strip() removes all whitespace from the start and end of p1
pl = node.get( 'points', '' ).strip()
if (pl == ''):
# If pl is blank there has been an error, return -1,-1 to indicate a problem has occured
return False, point
# Split string by whitespace
pa = pl.split()
if not len( pa ):
# If pl is blank there has been an error, return -1,-1 to indicate a problem has occured
return False, point
# pa[0] = "x1,y1
# split string via comma to get x1 and y1 individually
# then point = [x1,x2]
point = pa[0].split(",")
point = [float(point[0]),float(point[1])]
simpletransform.applyTransformToPoint(matNew, point)
return True, point
if node.tag == inkex.addNS( 'ellipse', 'svg' ) or node.tag == 'ellipse':
cx = float( node.get( 'cx', '0' ) )
cy = float( node.get( 'cy', '0' ) )
rx = float( node.get( 'rx', '0' ) )
point[0] = cx - rx
point[1] = cy
simpletransform.applyTransformToPoint(matNew, point)
return True, point
if node.tag == inkex.addNS( 'circle', 'svg' ) or node.tag == 'circle':
cx = float( node.get( 'cx', '0' ) )
cy = float( node.get( 'cy', '0' ) )
r = float( node.get( 'r', '0' ) )
point[0] = cx - r
point[1] = cy
simpletransform.applyTransformToPoint(matNew, point)
return True, point
if node.tag == inkex.addNS('symbol', 'svg') or node.tag == 'symbol':
# A symbol is much like a group, except that it should only be
# rendered when called within a "use" tag.
return False, point # Skip this element.
if node.tag == inkex.addNS('use', 'svg') or node.tag == 'use':
"""
A <use> element refers to another SVG element via an xlink:href="#blah"
attribute. We will handle the element by doing an XPath search through
the document, looking for the element with the matching id="blah"
attribute. We then recursively process that element after applying
any necessary (x,y) translation.
Notes:
1. We ignore the height and g attributes as they do not apply to
path-like elements, and
2. Even if the use element has visibility="hidden", SVG still calls
for processing the referenced element. The referenced element is
hidden only if its visibility is "inherit" or "hidden".
3. We may be able to unlink clones using the code in pathmodifier.py
"""
refid = node.get(inkex.addNS('href', 'xlink'))
if refid is not None:
# [1:] to ignore leading '#' in reference
path = '//*[@id="{0}"]'.format(refid[1:])
refnode = node.xpath(path)
if refnode is not None:
x = float(node.get('x', '0'))
y = float(node.get('y', '0'))
# Note: the transform has already been applied
if x != 0 or y != 0:
mat_new2 = simpletransform.composeTransform(matNew, simpletransform.parseTransform('translate({0:f},{1:f})'.format(x, y)))
else:
mat_new2 = matNew
if len(refnode) > 0:
plottable, the_point = self.group_last_pt(refnode[0], mat_new2)
else:
plottable, the_point = self.group_last_pt(refnode, mat_new2)
return plottable, the_point
except:
pass
# Svg Object is not a plottable element;
# Return False and a default point
return False, point
def group_first_pt(self, group, matCurrent = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
"""
Input: A Node which we have found to be a group
Output: Boolean value to indicate if a point is plottable
float values for first x,y coordinates of svg element
"""
if len(group) == 0: # Empty group -- The object may not be a group.
return self.getFirstPoint(group, matCurrent)
success = False
point = [float(-1), float(-1)]
# first apply the current matrix transform to this node's transform
matNew = simpletransform.composeTransform( matCurrent, simpletransform.parseTransform( group.get( "transform" ) ) )
# Step through the group, we examine each element until we find a plottable object
for subnode in group:
# Check to see if the subnode we are looking at in this iteration of our for loop is a group
# If it is a group, we must recursively call this function to search for a plottable object
if subnode.tag == inkex.addNS( 'g', 'svg' ) or subnode.tag == 'g':
# Verify that the nested group has objects within it
# otherwise we will not parse it
if subnode is not None:
# Check if group contains plottable elements by recursively calling group_first_pt
# If group contains plottable subnode, then it will return that value and escape the loop
# Else function continues search for first plottable object
success, point = self.group_first_pt(subnode, matNew)
if success:
# Subnode inside nested group is plottable!
# Break from our loop so we can return the first point of this plottable subnode
break
else:
continue
else:
# Node is not a group
# Get its first (x,y) coordinates
# Also get a Boolean value to indicate if the subnode is plottable or not
# If subnode is not plottable, continue to next subnode in the group
success, point = self.getFirstPoint(subnode, matNew)
if success:
# Subnode inside group is plottable!
# Break from our loop so we can return the first point of this plottable subnode
break
else:
continue
return success, point
def group_last_pt(self, group, matCurrent=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
"""
Input: A Node which we have found to be a group
Output: The last node within the group which can be plotted
"""
if len(group) == 0: # Empty group -- Did someone send an object that isn't a group?
return self.getLastPoint(group, matCurrent)
success = False
point = [float(-1),float(-1)]
# first apply the current matrix transform to this node's transform
matNew = simpletransform.composeTransform( matCurrent, simpletransform.parseTransform( group.get( "transform" ) ) )
# Step through the group, we examine each element until we find a plottable object
for subnode in reversed(group):
# Check to see if the subnode we are looking at in this iteration of our for loop is a group
# If it is a group, we must recursively call this function to search for a plottable object
if subnode.tag == inkex.addNS( 'g', 'svg' ) or subnode.tag == 'g':
# Verify that the nested group has objects within it
# otherwise we will not parse it
if subnode is not None:
# Check if group contains plottable elements by recursively calling group_last_pt
# If group contains plottable subnode, then it will return that value and escape the loop
# Else function continues search for last plottable object
success, point = self.group_last_pt(subnode, matNew)
if success:
# Subnode inside nested group is plottable!
# Break from our loop so we can return the first point of this plottable subnode
break
else:
continue
else:
# Node is not a group
# Get its first (x,y) coordinates
# Also get a Boolean value to indicate if the subnode is plottable or not
# If subnode is not plottable, continue to next subnode in the group
success, point = self.getLastPoint(subnode, matNew)
if success:
# Subode inside nested group is plottable!
# Break from our loop so we can return the first point of this plottable subnode
break
else:
continue
return success, point
def group2NodeDict(self, group, mat_current=None):
if mat_current is None:
mat_current = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
# first apply the current matrix transform to this node's transform
matNew = simpletransform.composeTransform( mat_current, simpletransform.parseTransform( group.get( "transform" ) ) )
nodes_in_group = []
# Step through the group, we examine each element until we find a plottable object
for subnode in group:
# Check to see if the subnode we are looking at in this iteration of our for loop is a group
# If it is a group, we must recursively call this function to search for a plottable object
if subnode.tag == inkex.addNS( 'g', 'svg' ) or subnode.tag == 'g':
# Verify that the nested group has objects within it
# otherwise we will not parse it
if subnode is not None:
# Check if group contains plottable elements by recursively calling group_first_pt
# If group contains plottable subnode, then it will return that value and escape the loop
# Else function continues search for first plottable object
nodes_in_group.extend(self.group2NodeDict(subnode, matNew))
else:
simpletransform.applyTransformToNode(matNew, subnode)
nodes_in_group.append(subnode)
return nodes_in_group
def ReorderNodeList(self, coord_dict, group_dict):
# Re-order the given set of SVG elements, using a simple "greedy" algorithm.
# The first object will be the element closest to the origin
# After this choice, the algorithm loops through all remaining elements looking for the element whose first x,y
# coordinates are closest to the the previous choice's last x,y coordinates
# This process continues until all elements have been sorted into ordered_element_list and removed from group_dict
ordered_layer_element_list = []
# Continue until all elements have been re-ordered
while group_dict:
nearest_dist = float('inf')
for key,node in group_dict.items():
# Is this node non-plottable?
# If so, exit loop and append element to ordered_layer_element_list
if not coord_dict[key][0]:
# Object is not Plottable
nearest = node
nearest_id = key
continue
# If we reach this point, node is plottable and needs to be considered in our algo
entry_x = coord_dict[key][1] # x-coordinate of first point of the path
entry_y = coord_dict[key][2] # y-coordinate of first point of the path
exit_x = coord_dict[key][3] # x-coordinate of last point of the path
exit_y = coord_dict[key][4] # y-coordinate of last point of the path
object_dist = (entry_x-self.x_last)*(entry_x-self.x_last) + (entry_y-self.y_last) * (entry_y-self.y_last)
# This is actually the distance squared; calculating it rather than the pythagorean distance
# saves a square root calculation. Right now, we only care about _which distance is less_
# not the exact value of it, so this is a harmless shortcut.
# If this distance is smaller than the previous element's distance, then replace the previous
# element's entry with our current element's distance
if nearest_dist >= object_dist:
# We have found an element closer than the previous closest element
nearest = node
nearest_id = key
nearest_dist = object_dist
nearest_start_x = entry_x
nearest_start_y = entry_y
# Now that the closest object has been determined, it is time to add it to the
# optimized list of closest objects
ordered_layer_element_list.append(nearest)
# To determine the closest object in the next iteration of the loop,
# we must save the last x,y coor of this element
# If this element is plottable, then save the x,y coordinates
# If this element is non-plottable, then do not save the x,y coordinates
if coord_dict[nearest_id][0]:
# Also, draw line indicating that we've found a new point.
if self.options.preview_rendering == True:
preview_path = [] # pen-up path data for preview
preview_path.append("M{0:.3f} {1:.3f}".format(
self.x_last, self.y_last))
preview_path.append("{0:.3f} {1:.3f}".format(
nearest_start_x, nearest_start_y))
self.p_style.update({'stroke': self.color_index(self.layer_index)})
path_attrs = {
'style': str(inkex.Style(self.p_style)),
'd': " ".join(preview_path)}
etree.SubElement( self.preview_layer,
inkex.addNS( 'path', 'svg'), path_attrs, nsmap=inkex.NSS )
self.x_last = coord_dict[nearest_id][3]
self.y_last = coord_dict[nearest_id][4]
# Remove this element from group_dict to indicate it has been optimized
del group_dict[nearest_id]
# Once all elements have been removed from the group_dictionary
# Return the optimized list of svg elements in the layer
return ordered_layer_element_list
def color_index(self, index):
index = index % 9
if index == 0:
return "rgb(255, 0, 0))"
elif index == 1:
return "rgb(170, 85, 0))"
elif index == 2:
return "rgb(85, 170, 0))"
elif index == 3:
return "rgb(0, 255, 0))"
elif index == 4:
return "rgb(0, 170, 85))"
elif index == 5:
return "rgb(0, 85, 170))"
elif index == 6:
return "rgb(0, 0, 255))"
elif index == 7:
return "rgb(85, 0, 170))"
else:
return "rgb(170, 0, 85))"
def getDocProps(self):
"""
Get the document's height and width attributes from the <svg> tag.
Use a default value in case the property is not present or is
expressed in units of percentages.
"""
self.svg_height = plot_utils.getLengthInches(self, 'height')
self.svg_width = plot_utils.getLengthInches(self, 'width')
width_string = self.svg.get('width')
if width_string:
value, units = plot_utils.parseLengthWithUnits(width_string)
self.doc_units = units
if self.auto_rotate and (self.svg_height > self.svg_width):
self.printPortrait = True
if self.svg_height is None or self.svg_width is None:
return False
else:
return True
def get_output(self):
# Return serialized copy of svg document output
result = etree.tostring(self.document)
return result.decode("utf-8")
# Create effect instance and apply it.
if __name__ == '__main__':
ReorderEffect().run()