def _convert(node): dashes = [] offset = 0 style = node.style if 'stroke-dasharray' in style: if style['stroke-dasharray'].find(',') > 0: dashes = [ float(dash) for dash in style['stroke-dasharray'].split(',') ] if 'stroke-dashoffset' in style: offset = style['stroke-dashoffset'] if not dashes: return new = [] for sub in node.path.to_superpath(): idash = 0 dash = dashes[0] length = float(offset) while dash < length: length = length - dash idash = (idash + 1) % len(dashes) dash = dashes[idash] new.append([sub[0][:]]) i = 1 while i < len(sub): dash = dash - length length = bezier.cspseglength(new[-1][-1], sub[i]) while dash < length: new[-1][-1], nxt, sub[i] = \ bezier.cspbezsplitatlength(new[-1][-1], sub[i], dash/length) if idash % 2: # create a gap new.append([nxt[:]]) else: # splice the curve new[-1].append(nxt[:]) length = length - dash idash = (idash + 1) % len(dashes) dash = dashes[idash] if idash % 2: new.append([sub[i]]) else: new[-1].append(sub[i]) i += 1 style.pop('stroke-dasharray') node.pop('sodipodi:type') node.path = CubicSuperPath(new) node.style = style
def split_into_nodes(self, nodes_number=1000): for id, node in self.svg.selected.items(): if node.tag == inkex.addNS('path', 'svg'): p = CubicSuperPath(node.get('d')) new = [] for sub in p: new.append([sub[0][:]]) i = 1 while i <= len(sub) - 1: length = bezier.cspseglength(new[-1][-1], sub[i]) splits = nodes_number for s in range(int(splits), 1, -1): new[-1][-1], next, sub[ i] = bezier.cspbezsplitatlength( new[-1][-1], sub[i], 1.0 / s) new[-1].append(next[:]) new[-1].append(sub[i]) i += 1 node.set('d', str(CubicSuperPath(new)))
def effect(self): for node in self.svg.selection.filter(PathElement): new = [] for sub in node.path.to_superpath(): new.append([sub[0][:]]) i = 1 while i <= len(sub) - 1: length = bezier.cspseglength(new[-1][-1], sub[i]) if self.options.method == 'bynum': splits = self.options.segments else: splits = math.ceil(length / self.options.max) for sel in range(int(splits), 1, -1): result = bezier.cspbezsplitatlength(new[-1][-1], sub[i], 1.0 / sel) better_result = [[list(el) for el in elements] for elements in result] new[-1][-1], nxt, sub[i] = better_result new[-1].append(nxt[:]) new[-1].append(sub[i]) i += 1 node.path = CubicSuperPath(new).to_path(curves_only=True)
def effect(self): for id, elem in self.svg.selected.items(): minlength = float(self.options.minlength) maxdist = float(self.options.maxdist) if self.options.minUse == False: minlength = 0 if self.options.joinEnd == False: maxdist = -1 #register which subpaths are closed dList = str(elem.path).upper().split(' M') closed = [""] l = 0 for sub in dList: if dList[l].find("Z") > -1: closed.append(" Z ") else: closed.append("") l += 1 closed.pop(0) new = [] s = 0 for sub in elem.path.to_superpath(): new.append([sub[0]]) i = 1 while i <= len(sub) - 1: length = bezier.cspseglength(new[-1][-1], sub[i]) #curve length if length >= minlength: new[-1].append(sub[i]) else: #average last node xy with this node xy and set this further node to last new[-1][-1][1][0] = 0.5 * (new[-1][-1][1][0] + sub[i][1][0]) new[-1][-1][1][1] = 0.5 * (new[-1][-1][1][1] + sub[i][1][1]) new[-1][-1][-1] = sub[i][-1] i += 1 if maxdist > -1: #calculate distance between first and last node #if <= maxdist set closed[i] to "Z " last = new[-1][-1] length = bezier.cspseglength(new[-1][-1], sub[0]) if length <= maxdist: newStartEnd = [ 0.5 * (new[-1][-1][-1][0] + new[0][0][0][0]), 0.5 * (new[-1][-1][-1][1] + new[0][0][0][1]) ] new[0][0][0] = newStartEnd new[0][0][1] = newStartEnd new[-1][-1][1] = newStartEnd new[-1][-1][2] = newStartEnd closed[s] = " Z " s += 1 elem.path = CubicSuperPath(new).to_path(curves_only=True) #reset z to the originally closed paths (z lost in cubicsuperpath) temppath = str(elem.path.to_absolute()).split('M ') temppath.pop(0) newPath = '' l = 0 for sub in temppath: newPath = newPath + 'M ' + temppath[l] + closed[l] l += 1 elem.path = newPath
def createLinks(node): nodeParent = node.getparent() pathIsClosed = False path = node.path.to_arrays() #to_arrays() is deprecated. How to make more modern? if path[-1][0] == 'Z' or path[0][1] == path[-1][1]: #if first is last point the path is also closed. The "Z" command is not required pathIsClosed = True if self.options.path_types == 'open_paths' and pathIsClosed is True: return #skip this loop iteration elif self.options.path_types == 'closed_paths' and pathIsClosed is False: return #skip this loop iteration elif self.options.path_types == 'both': pass # if keeping is enabled we make of copy of the current node and insert it while modifying the original ones. We could also delete the original and modify a copy... if self.options.keep_selected is True: parent = node.getparent() idx = parent.index(node) copynode = copy.copy(node) parent.insert(idx, copynode) # we measure the length of the path to calculate the required dash configuration csp = node.path.transform(node.composed_transform()).to_superpath() slengths, stotal = csplength(csp) #get segment lengths and total length of path in document's internal unit if self.options.length_filter is True: if stotal < self.svg.unittouu(str(self.options.length_filter_value) + self.options.length_filter_unit): if self.options.show_info is True: self.msg("node " + node.get('id') + " is shorter than minimum allowed length of {:1.3f} {}. Path length is {:1.3f} {}".format(self.options.length_filter_value, self.options.length_filter_unit, stotal, self.options.creationunit)) return #skip this loop iteration if self.options.creationunit == "percent": length_link = (self.options.length_link / 100.0) * stotal else: length_link = self.svg.unittouu(str(self.options.length_link) + self.options.creationunit) dashes = [] #central dashes array if self.options.creationtype == "entered_values": dash_length = ((stotal - length_link * self.options.link_count) / self.options.link_count) - 2 * length_link * self.options.link_multiplicator dashes.append(dash_length) dashes.append(length_link) for i in range(0, self.options.link_multiplicator): dashes.append(length_link) #stroke (=gap) dashes.append(length_link) #gap if self.options.switch_pattern is True: dashes = dashes[::-1] #reverse the array #validate dashes. May not be negative (dash or gap cannot be longer than the path itself). Otherwise Inkscape will freeze forever. Reason: rendering issue if any(dash <= 0.0 for dash in dashes) == True: if self.options.show_info is True: self.msg("node " + node.get('id') + ": Error! Dash array may not contain negative numbers: " + ' '.join(format(dash, "1.3f") for dash in dashes) + ". Path skipped. Maybe it's too short. Adjust your link count, multiplicator and length accordingly, or set to unit '%'") return False if self.options.skip_errors is True else exit(1) if self.options.creationunit == "percent": stroke_dashoffset = self.options.link_offset / 100.0 else: stroke_dashoffset = self.svg.unittouu(str(self.options.link_offset) + self.options.creationunit) if self.options.switch_pattern is True: stroke_dashoffset = stroke_dashoffset + ((self.options.link_multiplicator * 2) + 1) * length_link if self.options.creationtype == "use_existing": if self.options.no_convert is True: if self.options.show_info is True: self.msg("node " + node.get('id') + ": Nothing to do. Please select another creation method or disable cosmetic style output paths.") return False if self.options.skip_errors is True else exit(1) stroke_dashoffset = 0 style = node.style if 'stroke-dashoffset' in style: stroke_dashoffset = style['stroke-dashoffset'] try: floats = [float(dash) for dash in re.findall(r"[+]?\d*\.\d+|\d+", style['stroke-dasharray'])] #allow only positive values if len(floats) > 0: dashes = floats #overwrite previously calculated values with custom input else: raise ValueError except: if self.options.show_info is True: self.msg("node " + node.get('id') + ": No dash style to continue with.") return False if self.options.skip_errors is True else exit(1) if self.options.creationtype == "custom_dashpattern": stroke_dashoffset = self.options.custom_dashoffset_value try: floats = [float(dash) for dash in re.findall(r"[+]?\d*\.\d+|\d+", self.options.custom_dasharray_value)] #allow only positive values if len(floats) > 0: dashes = floats #overwrite previously calculated values with custom input else: raise ValueError except: if self.options.show_info is True: self.msg("node " + node.get('id') + ": Error in custom dasharray string (might be empty or does not contain any numbers).") return False if self.options.skip_errors is True else exit(1) #assign stroke dasharray from entered values, existing style or custom dashpattern stroke_dasharray = ' '.join(format(dash, "1.3f") for dash in dashes) # check if the node has a style attribute. If not we create a blank one with a black stroke and without fill style = None default_fill = 'none' default_stroke_width = '1px' default_stroke = '#000000' if node.attrib.has_key('style'): style = node.get('style') if style.endswith(';') is False: style += ';' # if has style attribute and dasharray and/or dashoffset are present we modify it accordingly declarations = style.split(';') # parse the style content and check what we need to adjust for i, decl in enumerate(declarations): parts = decl.split(':', 2) if len(parts) == 2: (prop, val) = parts prop = prop.strip().lower() #if prop == 'fill': # declarations[i] = prop + ':{}'.format(default_fill) #if prop == 'stroke': # declarations[i] = prop + ':{}'.format(default_stroke) #if prop == 'stroke-width': # declarations[i] = prop + ':{}'.format(default_stroke_width) if prop == 'stroke-dasharray': #comma separated list of one or more float values declarations[i] = prop + ':{}'.format(stroke_dasharray) if prop == 'stroke-dashoffset': declarations[i] = prop + ':{}'.format(stroke_dashoffset) node.set('style', ';'.join(declarations)) #apply new style to node #if has style attribute but the style attribute does not contain fill, stroke, stroke-width, stroke-dasharray or stroke-dashoffset yet style = node.style if re.search('fill:(.*?)(;|$)', str(style)) is None: style += 'fill:{};'.format(default_fill) if re.search('(;|^)stroke:(.*?)(;|$)', str(style)) is None: #if "stroke" is None, add one. We need to distinguish because there's also attribute "-inkscape-stroke" that's why we check starting with ^ or ; style += 'stroke:{};'.format(default_stroke) if not 'stroke-width' in style: style += 'stroke-width:{};'.format(default_stroke_width) if not 'stroke-dasharray' in style: style += 'stroke-dasharray:{};'.format(stroke_dasharray) if not 'stroke-dashoffset' in style: style += 'stroke-dashoffset:{};'.format(stroke_dashoffset) node.set('style', style) else: style = 'fill:{};stroke:{};stroke-width:{};stroke-dasharray:{};stroke-dashoffset:{};'.format(default_fill, default_stroke, default_stroke_width, stroke_dasharray, stroke_dashoffset) node.set('style', style) # Print some info about values if self.options.show_info is True: self.msg("node " + node.get('id') + ":") if self.options.creationunit == "percent": self.msg(" * total path length = {:1.3f} {}".format(stotal, self.svg.unit)) #show length, converted in selected unit self.msg(" * (calculated) offset: {:1.3f} %".format(stroke_dashoffset)) if self.options.creationtype == "entered_values": self.msg(" * (calculated) gap length: {:1.3f} %".format(length_link)) else: self.msg(" * total path length = {:1.3f} {} ({:1.3f} {})".format(self.svg.uutounit(stotal, self.options.creationunit), self.options.creationunit, stotal, self.svg.unit)) #show length, converted in selected unit self.msg(" * (calculated) offset: {:1.3f} {}".format(self.svg.uutounit(stroke_dashoffset, self.options.creationunit), self.options.creationunit)) if self.options.creationtype == "entered_values": self.msg(" * (calculated) gap length: {:1.3f} {}".format(length_link, self.options.creationunit)) if self.options.creationtype == "entered_values": self.msg(" * total gaps = {}".format(self.options.link_count)) self.msg(" * (calculated) dash/gap pattern: {} ({})".format(stroke_dasharray, self.svg.unit)) # Conversion step (split cosmetic path into real segments) if self.options.no_convert is False: style = node.style #get the style again, but this time as style class new = [] for sub in node.path.to_superpath(): idash = 0 dash = dashes[0] length = float(stroke_dashoffset) while dash < length: length = length - dash idash = (idash + 1) % len(dashes) dash = dashes[idash] new.append([sub[0][:]]) i = 1 while i < len(sub): dash = dash - length length = bezier.cspseglength(new[-1][-1], sub[i]) while dash < length: new[-1][-1], nxt, sub[i] = bezier.cspbezsplitatlength(new[-1][-1], sub[i], dash/length) if idash % 2: # create a gap new.append([nxt[:]]) else: # splice the curve new[-1].append(nxt[:]) length = length - dash idash = (idash + 1) % len(dashes) dash = dashes[idash] if idash % 2: new.append([sub[i]]) else: new[-1].append(sub[i]) i += 1 style.pop('stroke-dasharray') node.pop('sodipodi:type') csp = CubicSuperPath(new) node.path = CubicSuperPath(new) node.style = style # break apart the combined path to have multiple elements if self.options.breakapart is True: breakOutputNodes = None breakOutputNodes = self.breakContours(node, breakOutputNodes) breakApartGroup = nodeParent.add(inkex.Group()) for breakOutputNode in breakOutputNodes: breakApartGroup.append(breakOutputNode) #self.msg(replacedNode.get('id')) #self.svg.selection.set(replacedNode.get('id')) #update selection to split paths segments (does not work, so commented out) #cleanup useless points p = breakOutputNode.path commandsCoords = p.to_arrays() # "m 45.250809,91.692739" - this path contains onyl one command - a single point if len(commandsCoords) == 1: breakOutputNode.delete() # "m 45.250809,91.692739 z" - this path contains two commands, but only one coordinate. # It's a single point, the path is closed by a Z command elif len(commandsCoords) == 2 and commandsCoords[0][1] == commandsCoords[1][1]: breakOutputNode.delete() # "m 45.250809,91.692739 l 45.250809,91.692739" - this path contains two commands, # but the first and second coordinate are the same. It will render als point elif len(commandsCoords) == 2 and commandsCoords[-1][0] == 'Z': breakOutputNode.delete() # "m 45.250809,91.692739 l 45.250809,91.692739 z" - this path contains three commands, # but the first and second coordinate are the same. It will render als point, the path is closed by a Z command elif len(commandsCoords) == 3 and commandsCoords[0][1] == commandsCoords[1][1] and commandsCoords[2][1] == 'Z': breakOutputNode.delete()