Mario Voigt 2021-11-24 00:38:41 +01:00
commit d6409fda72
14 changed files with 343 additions and 123 deletions

View File

@ -36,7 +36,7 @@ DETACHED_PROCESS = 0x00000008
class AnimateOrder(inkex.EffectExtension): class AnimateOrder(inkex.EffectExtension):
def spawnIndependentProcess(self, args): #function to spawn non-blocking inkscape instance. the inkscape command is available because it is added to ENVIRONMENT when Inkscape main instance is started def spawnIndependentProcess(self, args):
warnings.simplefilter('ignore', ResourceWarning) #suppress "enable tracemalloc to get the object allocation traceback" warnings.simplefilter('ignore', ResourceWarning) #suppress "enable tracemalloc to get the object allocation traceback"
if os.name == 'nt': if os.name == 'nt':
subprocess.Popen(args, close_fds=True, creationflags=DETACHED_PROCESS) subprocess.Popen(args, close_fds=True, creationflags=DETACHED_PROCESS)

View File

@ -3,7 +3,13 @@
<name>Draw Directions / Travel Moves</name> <name>Draw Directions / Travel Moves</name>
<id>fablabchemnitz.de.draw_directions_tavel_moves</id> <id>fablabchemnitz.de.draw_directions_tavel_moves</id>
<param name="nb_main" type="notebook"> <param name="nb_main" type="notebook">
<page name="tab_settings" gui-text="Settings"> <page name="tab_draw" gui-text="Draw">
<param name="order" type="optiongroup" appearance="combo" gui-text="Order">
<option value="separate_groups">To separate groups</option>
<option value="separate_layers">To separate layers</option>
<option value="element_index">At element's index</option>
</param>
<separator/>
<param name="draw_dots" type="bool" gui-text="Draw dots" gui-description="Start and end point of opened (red + blue) and closed paths (green + yellow)">true</param> <param name="draw_dots" type="bool" gui-text="Draw dots" gui-description="Start and end point of opened (red + blue) and closed paths (green + yellow)">true</param>
<param name="dotsize" type="int" min="1" max="9999" gui-text="Dot size (px)">10</param> <param name="dotsize" type="int" min="1" max="9999" gui-text="Dot size (px)">10</param>
<separator/> <separator/>
@ -15,9 +21,12 @@
<separator/> <separator/>
<param name="debug" type="bool" gui-text="Print debug info">false</param> <param name="debug" type="bool" gui-text="Print debug info">false</param>
</page> </page>
<page name="tab_remove" gui-text="Remove">
<label>If this page is selected, you can remove all old travel lines/groups Just press 'apply'.</label>
</page>
<page name="tab_about" gui-text="About"> <page name="tab_about" gui-text="About">
<label appearance="header">Draw Directions / Travel Moves</label> <label appearance="header">Draw Directions / Travel Moves</label>
<label>Draw travel lines and/or dots to mark start and end points of paths</label> <label>Draw travel lines and/or dots to mark start and end points of paths and to show the line order.</label>
<label>2021 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label> <label>2021 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
<spacer/> <spacer/>
<label appearance="header">Online Documentation</label> <label appearance="header">Online Documentation</label>

View File

@ -6,21 +6,22 @@ import inkex
from inkex import Circle, Vector2d from inkex import Circle, Vector2d
from inkex.paths import Path from inkex.paths import Path
from inkex.bezier import csplength from inkex.bezier import csplength
from debugpy.common.timestamp import current
''' '''
ToDos ToDos
- draw numbers for each travel lines next to the line - draw numbers for each travel lines next to the line
- option selection: add travel lines by color groups or add them at the index of the line -> this keeps the order in total (will need transform of start point and second transform of end point, if they are in different groups)
- option to just remove all generated travelLines/dots (especially hard to delete if they are inserted at element order)
''' '''
class DrawDirectionsTravelMoves(inkex.EffectExtension): class DrawDirectionsTravelMoves(inkex.EffectExtension):
def drawCircle(self, group, color, point): def drawCircle(self, group, color, point):
style = inkex.Style({'stroke': 'none', 'fill': color}) style = inkex.Style({'stroke': 'none', 'fill': color})
startCircle = group.add(Circle(cx=str(point[0]), cy=str(point[1]), r=str(self.svg.unittouu(str(so.dotsize/2) + "px")))) startCircle = group.add(Circle(cx=str(point[0]), cy=str(point[1]), r=str(self.svg.unittouu(str(so.dotsize/2) + "px"))))
startCircle.style = style startCircle.style = style
def find_group(self, groupId): def find_group(self, groupId):
''' check if a group with a given id exists or not. Returns None if not found, else returns the group element ''' ''' check if a group with a given id exists or not. Returns None if not found, else returns the group element '''
groups = self.document.xpath('//svg:g', namespaces=inkex.NSS) groups = self.document.xpath('//svg:g', namespaces=inkex.NSS)
@ -30,50 +31,8 @@ class DrawDirectionsTravelMoves(inkex.EffectExtension):
return group return group
return None return None
def add_arguments(self, pars):
pars.add_argument("--nb_main")
pars.add_argument("--draw_dots", type=inkex.Boolean, default=True, help="Start and end point of opened (red + blue) and closed paths (green + yellow)") def createTravelMarker(self, markerId):
pars.add_argument("--dotsize", type=int, default=10, help="Dot size (px)")
pars.add_argument("--draw_travel_moves", type=inkex.Boolean, default=False)
pars.add_argument("--ignore_colors", type=inkex.Boolean, default=False, help="If enabled we connect all lines by order, ignoring the stroke color (normally we have one travel line group per color)")
pars.add_argument("--dashed_style", type=inkex.Boolean, default=True)
pars.add_argument("--arrow_style", type=inkex.Boolean, default=True)
pars.add_argument("--arrow_size", type=float, default=True)
pars.add_argument("--debug", type=inkex.Boolean, default=False)
def effect(self):
global so
so = self.options
linePrefix = "travelLine-"
groupPrefix = "travelLines-"
ignoreWord = "ignore"
selectedPaths = [] #total list of elements to parse
def parseChildren(element):
if isinstance(element, inkex.PathElement) and element not in selectedPaths and linePrefix not in element.get('id') and not isinstance(element.getparent(), inkex.Marker):
selectedPaths.append(element)
children = element.getchildren()
if children is not None:
for child in children:
if isinstance(child, inkex.PathElement) and child not in selectedPaths and linePrefix not in element.get('id') and not isinstance(element.getparent(), inkex.Marker):
selectedPaths.append(child)
parseChildren(child) #go deeper and deeper
#check if we have selectedPaths elements or if we should parse the whole document instead
if len(self.svg.selected) == 0:
for element in self.document.getroot().iter(tag=etree.Element):
if isinstance(element, inkex.PathElement) and element != self.document.getroot() and linePrefix not in element.get('id') and not isinstance(element.getparent(), inkex.Marker):
selectedPaths.append(element)
else:
for element in self.svg.selected.values():
parseChildren(element)
dot_group = self.svg.add(inkex.Group())
if so.arrow_style is True:
markerId = "travel_move_arrow"
#add new marker to defs (or overwrite if existent) #add new marker to defs (or overwrite if existent)
defs = self.svg.defs defs = self.svg.defs
for defi in defs: for defi in defs:
@ -97,10 +56,92 @@ class DrawDirectionsTravelMoves(inkex.EffectExtension):
marker.append(markerPath) marker.append(markerPath)
defs.append(marker) #do not append before letting contain it one path. Otherwise Inkscape is going to crash immediately defs.append(marker) #do not append before letting contain it one path. Otherwise Inkscape is going to crash immediately
def add_arguments(self, pars):
pars.add_argument("--nb_main")
pars.add_argument("--order", default="separate_groups")
pars.add_argument("--draw_dots", type=inkex.Boolean, default=True, help="Start and end point of opened (red + blue) and closed paths (green + yellow)")
pars.add_argument("--dotsize", type=int, default=10, help="Dot size (px)")
pars.add_argument("--draw_travel_moves", type=inkex.Boolean, default=False)
pars.add_argument("--ignore_colors", type=inkex.Boolean, default=False, help="If enabled we connect all lines by order, ignoring the stroke color (normally we have one travel line group per color)")
pars.add_argument("--dashed_style", type=inkex.Boolean, default=True)
pars.add_argument("--arrow_style", type=inkex.Boolean, default=True)
pars.add_argument("--arrow_size", type=float, default=True)
pars.add_argument("--debug", type=inkex.Boolean, default=False)
def effect(self):
global so
so = self.options
dotPrefix = "dots-"
lineSuffix = "-travelLine"
groupPrefix = "travelLines-"
ignoreWord = "ignore"
if so.nb_main == "tab_remove":
#remove dots
dots = self.document.xpath("//svg:g[starts-with(@id, '" + dotPrefix + "')]", namespaces=inkex.NSS)
for dot in dots:
dot.delete()
#remove travel lines
travelLines = self.document.xpath("//svg:path[contains(@id, '" + lineSuffix + "')]", namespaces=inkex.NSS)
for travelLine in travelLines:
travelLine.delete()
#remove travel groups/layers
travelGroups = self.document.xpath("//svg:g[starts-with(@id, '" + groupPrefix + "')]", namespaces=inkex.NSS)
for travelGroup in travelGroups:
if len(travelGroup) == 0:
travelGroup.delete()
return
#else ...
selectedPaths = [] #total list of elements to parse
def parseChildren(element):
if isinstance(element, inkex.PathElement) and element not in selectedPaths and lineSuffix not in element.get('id') and not isinstance(element.getparent(), inkex.Marker):
selectedPaths.append(element)
children = element.getchildren()
if children is not None:
for child in children:
if isinstance(child, inkex.PathElement) and child not in selectedPaths and lineSuffix not in element.get('id') and not isinstance(element.getparent(), inkex.Marker):
selectedPaths.append(child)
parseChildren(child) #go deeper and deeper
#check if we have selectedPaths elements or if we should parse the whole document instead
if len(self.svg.selected) == 0:
for element in self.document.getroot().iter(tag=etree.Element):
if isinstance(element, inkex.PathElement) and element != self.document.getroot() and lineSuffix not in element.get('id') and not isinstance(element.getparent(), inkex.Marker):
selectedPaths.append(element)
else:
for element in self.svg.selected.values():
parseChildren(element)
dotGroup = self.svg.add(inkex.Group(id=self.svg.get_unique_id(dotPrefix)))
if so.order == "separate_layers":
dotGroup.set("inkscape:groupmode", "layer")
dotGroup.set("inkscape:label", dotPrefix + "layer")
if so.order == "separate_groups":
dotGroup.pop("inkscape:groupmode")
dotGroup.pop("inkscape:label")
if dotGroup.style.get('display') is not None:
dotGroup.style.pop("display") #if the group previously has been a layer (which was hidden), a display:none will be added. we don't want that
if so.arrow_style is True:
markerId = "travel_move_arrow"
self.createTravelMarker(markerId)
#collect all different stroke colors to distinguish by groups #collect all different stroke colors to distinguish by groups
strokeColors = [] strokeColors = []
strokeColorsAndCounts = {} strokeColorsAndCounts = {}
startEndPath = [] #the container for all paths we will loop on '''
the container for all paths we will loop on. Index:
0 = element
1 = start point
2 = end point
'''
startEndPath = []
for element in selectedPaths: for element in selectedPaths:
strokeColor = element.style.get('stroke') strokeColor = element.style.get('stroke')
@ -112,11 +153,21 @@ class DrawDirectionsTravelMoves(inkex.EffectExtension):
groupName = groupPrefix + strokeColor groupName = groupPrefix + strokeColor
travelGroup = self.find_group(groupName) travelGroup = self.find_group(groupName)
if travelGroup is None: if travelGroup is None:
self.document.getroot().append(inkex.Group(id=groupName)) travelGroup = inkex.Group(id=groupName)
self.document.getroot().append(travelGroup)
else: #exists else: #exists
self.document.getroot().append(travelGroup) self.document.getroot().append(travelGroup)
for child in travelGroup: for child in travelGroup:
child.delete() child.delete()
if so.order == "separate_layers":
travelGroup.set("inkscape:groupmode", "layer")
travelGroup.set("inkscape:label", groupName + "-layer")
if so.order == "separate_groups":
travelGroup.pop("inkscape:groupmode")
travelGroup.pop("inkscape:label")
if travelGroup.style.get('display') is not None:
travelGroup.style.pop("display")
p = element.path.transform(element.composed_transform()) p = element.path.transform(element.composed_transform())
points = list(p.end_points) points = list(p.end_points)
@ -127,11 +178,11 @@ class DrawDirectionsTravelMoves(inkex.EffectExtension):
if so.draw_dots is True: if so.draw_dots is True:
if start[0] == end[0] and start[1] == end[1]: if start[0] == end[0] and start[1] == end[1]:
self.drawCircle(dot_group, '#00FF00', start) self.drawCircle(dotGroup, '#00FF00', start)
self.drawCircle(dot_group, '#FFFF00', points[1]) #draw one point which gives direction of the path self.drawCircle(dotGroup, '#FFFF00', points[1]) #draw one point which gives direction of the path
else: #open contour with start and end point else: #open contour with start and end point
self.drawCircle(dot_group, '#FF0000', start) self.drawCircle(dotGroup, '#FF0000', start)
self.drawCircle( dot_group, '#0000FF', end) self.drawCircle( dotGroup, '#0000FF', end)
for strokeColor in strokeColors: for strokeColor in strokeColors:
if strokeColor in strokeColorsAndCounts: if strokeColor in strokeColorsAndCounts:
@ -149,8 +200,22 @@ class DrawDirectionsTravelMoves(inkex.EffectExtension):
for i in range(0, ran): #loop through the item selection. nested loop. so we loop over alle elements again for each color for i in range(0, ran): #loop through the item selection. nested loop. so we loop over alle elements again for each color
if i < ran - 1: if i < ran - 1:
elementStroke = startEndPath[i][0].style.get('stroke') elementStroke = startEndPath[i][0].style.get('stroke')
currentElement = startEndPath[i][0]
idx = currentElement.getparent().index(currentElement)
travelLineId = currentElement.get('id') + lineSuffix + ("-begin" if firstOccurence is True else "")
if i == ran or createdMoves == colorCount: if i == ran or createdMoves == colorCount:
elementStroke = startEndPath[i-1][0].style.get('stroke') elementStroke = startEndPath[i-1][0].style.get('stroke')
currentElement = startEndPath[i-1][0]
idx = currentElement.getparent().index(currentElement) + 1
travelLineId = currentElement.get('id') + lineSuffix + "-end"
if i < ran - 2:
nextElement = startEndPath[i+1][0]
elif i < ran - 1:
nextElement = startEndPath[i][0]
else:
nextElement = None
if elementStroke is None or elementStroke == "none": if elementStroke is None or elementStroke == "none":
elementStroke = "none" elementStroke = "none"
@ -177,11 +242,10 @@ class DrawDirectionsTravelMoves(inkex.EffectExtension):
if i == ran - 1: if i == ran - 1:
inkex.utils.debug("segment={},{}".format(startEndPath[i-1][1], travelEnd)) inkex.utils.debug("segment={},{}".format(startEndPath[i-1][1], travelEnd))
travelLine = inkex.PathElement(id=self.svg.get_unique_id(linePrefix) + element.get('id')) travelLine = inkex.PathElement(id=travelLineId)
#if some objects are at svg:svg level this may cause errors #if some objects are at svg:svg level this may cause errors
#if element.getparent() != self.document.getroot(): #if element.getparent() != self.document.getroot():
# travelLine.transform = element.getparent().composed_transform() # travelLine.transform = element.getparent().composed_transform()
travelLine.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(travelStart[0],travelStart[1],travelEnd[0],travelEnd[1]))
travelLine.style = {'stroke': ("#000000" if so.ignore_colors is True else sc), 'fill': 'none', 'stroke-width': str(self.svg.unittouu('1px')), 'marker-end': 'url(#marker1426)'} travelLine.style = {'stroke': ("#000000" if so.ignore_colors is True else sc), 'fill': 'none', 'stroke-width': str(self.svg.unittouu('1px')), 'marker-end': 'url(#marker1426)'}
if so.dashed_style is True: if so.dashed_style is True:
travelLine.style['stroke-dasharray'] = '1,1' travelLine.style['stroke-dasharray'] = '1,1'
@ -189,12 +253,40 @@ class DrawDirectionsTravelMoves(inkex.EffectExtension):
if so.arrow_style is True: if so.arrow_style is True:
travelLine.style["marker-end"] = "url(#{})".format(markerId) travelLine.style["marker-end"] = "url(#{})".format(markerId)
#this is a really dirty block of code
if so.order == "element_index":
#adding the lines at element index requires to apply transformations for start point and end point (in case they are in different groups)
pseudo1 = inkex.PathElement()
pseudo1.set('d', "M{:0.6f},{:0.6f}".format(travelStart[0],travelStart[1]))
pseudo2 = inkex.PathElement()
pseudo2.set('d', "M{:0.6f},{:0.6f}".format(travelEnd[0],travelEnd[1]))
if nextElement is not None:
if currentElement.getparent() == nextElement.getparent():
pseudo1.path = pseudo1.path.transform(-currentElement.composed_transform()).to_superpath()
pseudo2.path = pseudo2.path.transform(-nextElement.composed_transform()).to_superpath()
else:
pseudo1.path = pseudo1.path.transform(-currentElement.composed_transform()).to_superpath()
pseudo2.path = pseudo2.path.transform(-currentElement.composed_transform()).to_superpath()
else:
pseudo1.path = pseudo1.path.transform(-currentElement.composed_transform()).to_superpath()
pseudo2.path = pseudo2.path.transform(-currentElement.composed_transform()).to_superpath()
travelLine.path = pseudo1.path + pseudo2.get('d').replace("M", "L")
if so.debug is True: self.msg("travelLine={}".format(travelLine.path))
#check the length. if zero we do not add #check the length. if zero we do not add
slengths, stotal = csplength(travelLine.path.transform(element.composed_transform()).to_superpath()) #get segment lengths and total length of path in document's internal unit slengths, stotal = csplength(travelLine.path.transform(currentElement.composed_transform()).to_superpath()) #get segment lengths and total length of path in document's internal unit
if stotal > 0: if stotal > 0:
#finally add the line #finally add the line
travelGroup = self.find_group(groupPrefix + sc) currentElement.getparent().insert(idx, travelLine)
travelGroup.add(travelLine) else:
if so.debug is True: inkex.utils.debug("Line has length of zero")
else:
travelLine.set('d', "M{:0.6f},{:0.6f} L{:0.6f},{:0.6f}".format(travelStart[0],travelStart[1],travelEnd[0],travelEnd[1]))
#check the length. if zero we do not add
slengths, stotal = csplength(travelLine.path.transform(currentElement.composed_transform()).to_superpath()) #get segment lengths and total length of path in document's internal unit
if stotal > 0:
#finally add the line
self.find_group(groupPrefix + sc).add(travelLine)
else: else:
if so.debug is True: inkex.utils.debug("Line has length of zero") if so.debug is True: inkex.utils.debug("Line has length of zero")
@ -203,8 +295,8 @@ class DrawDirectionsTravelMoves(inkex.EffectExtension):
if so.debug is True: inkex.utils.debug("-"*40) if so.debug is True: inkex.utils.debug("-"*40)
#cleanup empty groups #cleanup empty groups
if len(dot_group) == 0: if len(dotGroup) == 0:
dot_group.delete() dotGroup.delete()
travelGroups = self.document.xpath("//svg:g[starts-with(@id, 'travelLines-')]", namespaces=inkex.NSS) travelGroups = self.document.xpath("//svg:g[starts-with(@id, 'travelLines-')]", namespaces=inkex.NSS)
for travelGroup in travelGroups: for travelGroup in travelGroups:
if len(travelGroup) == 0: if len(travelGroup) == 0:

View File

@ -7,7 +7,8 @@
<label appearance="header">General Settings</label> <label appearance="header">General Settings</label>
<param name="debug" type="bool" gui-text="Enable debug">false</param> <param name="debug" type="bool" gui-text="Enable debug">false</param>
<param name="apply_transformations" type="bool" gui-text="Apply transformations (requires separate extension)" gui-description="This will call the extension 'Apply Transformations'. Helps avoiding geometry shifting">false</param> <param name="apply_transformations" type="bool" gui-text="Apply transformations (requires separate extension)" gui-description="This will call the extension 'Apply Transformations'. Helps avoiding geometry shifting">false</param>
<param name="breakapart" type="bool" gui-text="Break apart output path(s) into segments" gui-description="Performs CTRL + SHIFT + K to break the new output path into it's parts. Recommended to enable because default break apart of Inkscape might produce pointy paths.">true</param> <param name="breakapart" type="bool" gui-text="Break apart selected path(s) into segments" gui-description="Performs CTRL + SHIFT + K to break paths into parts">true</param>
<param name="breakapart_total" type="bool" gui-text="Break segments to lines" gui-description="Gives the best results for nodes/&lt;interval&gt; filtering">true</param>
<hbox> <hbox>
<vbox> <vbox>
<label appearance="header">Threshold</label> <label appearance="header">Threshold</label>

View File

@ -30,7 +30,8 @@ class FilterByLengthArea(inkex.EffectExtension):
pars.add_argument('--tab') pars.add_argument('--tab')
pars.add_argument('--debug', type=inkex.Boolean, default=False) pars.add_argument('--debug', type=inkex.Boolean, default=False)
pars.add_argument("--apply_transformations", type=inkex.Boolean, default=False, help="Run 'Apply Transformations' extension before running vpype. Helps avoiding geometry shifting") pars.add_argument("--apply_transformations", type=inkex.Boolean, default=False, help="Run 'Apply Transformations' extension before running vpype. Helps avoiding geometry shifting")
pars.add_argument("--breakapart", type=inkex.Boolean, default=True, help="Performs CTRL + SHIFT + K to break the new output path into it's parts. Recommended to enable because default break apart of Inkscape might produce pointy paths.") pars.add_argument("--breakapart", type=inkex.Boolean, default=True, help="Break apart selected path(s) into segments")
pars.add_argument("--breakapart_total", type=inkex.Boolean, default=True, help="Gives the best results for nodes/<interval> filtering")
pars.add_argument("--cleanup", type=inkex.Boolean, default = True, help = "Cleanup all unused groups/layers (requires separate extension)") pars.add_argument("--cleanup", type=inkex.Boolean, default = True, help = "Cleanup all unused groups/layers (requires separate extension)")
pars.add_argument('--unit') pars.add_argument('--unit')
pars.add_argument('--min_filter_enable', type=inkex.Boolean, default=True, help='Enable filtering min.') pars.add_argument('--min_filter_enable', type=inkex.Boolean, default=True, help='Enable filtering min.')
@ -63,21 +64,40 @@ class FilterByLengthArea(inkex.EffectExtension):
idSuffix = 0 idSuffix = 0
raw = element.path.to_arrays() raw = element.path.to_arrays()
subPaths, prev = [], 0 subPaths, prev = [], 0
if self.options.breakapart_total is False:
for i in range(len(raw)): # Breaks compound paths into simple paths for i in range(len(raw)): # Breaks compound paths into simple paths
if raw[i][0] == 'M' and i != 0: if raw[i][0] == 'M' and i != 0:
subPaths.append(raw[prev:i]) subPaths.append(raw[prev:i])
prev = i prev = i
subPaths.append(raw[prev:]) subPaths.append(raw[prev:])
else:
rawCopy = element.path.to_arrays() #we need another set of the same path
for i in range(len(raw)): # Breaks compound paths into simple paths
if i != 0:
if raw[i][0] == 'C':
rawCopy[i][1] = [raw[i][1][-2], raw[i][1][-1]]
elif raw[i][0] == 'L':
rawCopy[i][1] = [raw[i][1][0], raw[i][1][1]]
elif raw[i][0] == 'Z': #replace Z with another L command (which moves to the coordinates of the first M command in path) to have better overview
raw[-1][0] = 'L'
raw[-1][1] = raw[0][1]
rawCopy[i][0] = 'M' #we really need M. Does not matter if 'L' or 'C'.
#self.msg("s1={},s2={}".format(rawCopy[i-1], raw[i]))
subPaths.append([rawCopy[i-1], raw[i]])
prev = i
subPaths = subPaths[::-1]
for subpath in subPaths: for subpath in subPaths:
#self.msg(subpath)
replacedelement = copy.copy(element) replacedelement = copy.copy(element)
oldId = replacedelement.get('id') oldId = replacedelement.get('id')
csp = CubicSuperPath(subpath) csp = CubicSuperPath(subpath)
if len(subpath) > 1 and csp[0][0] != csp[0][1]: #avoids pointy paths like M "31.4794 57.6024 Z" if len(subpath) > 1 and csp[0][0] != csp[0][1]: #avoids pointy paths like M "31.4794 57.6024 Z"
replacedelement.set('d', csp) replacedelement.set('d', csp)
if len(subPaths) == 1: if len(subPaths) == 1:
replacedelement.set('id', oldId) replacedelement.set('id', "{}".format(oldId))
else: else:
replacedelement.set('id', oldId + str(idSuffix)) replacedelement.set('id', "{}-{}".format(oldId, str(idSuffix)))
idSuffix += 1 idSuffix += 1
parent.insert(idx, replacedelement) parent.insert(idx, replacedelement)
breakelements.append(replacedelement) breakelements.append(replacedelement)
@ -116,7 +136,7 @@ class FilterByLengthArea(inkex.EffectExtension):
elements.extend(self.breakContours(element, None)) elements.extend(self.breakContours(element, None))
if so.debug is True: if so.debug is True:
inkex.utils.debug("Collecting elements ...") inkex.utils.debug("Collecting svg:path elements ...")
for element in elements: for element in elements:
# additional option to apply transformations. As we clear up some groups to form new layers, we might lose translations, rotations, etc. # additional option to apply transformations. As we clear up some groups to form new layers, we might lose translations, rotations, etc.
@ -229,7 +249,10 @@ class FilterByLengthArea(inkex.EffectExtension):
if so.group is True: if so.group is True:
group.append(element) group.append(element)
if so.cleanup == True: #if len(group) == 0:
# group.delete()
if so.cleanup is True:
try: try:
import remove_empty_groups import remove_empty_groups
remove_empty_groups.RemoveEmptyGroups.effect(self) remove_empty_groups.RemoveEmptyGroups.effect(self)

View File

@ -5,6 +5,7 @@
<param name="begin_str" type="string" gui-text="begin_str:">0</param> <param name="begin_str" type="string" gui-text="begin_str:">0</param>
<param name="repeat_str" type="string" gui-text="repeat_str:">indefinite</param> <param name="repeat_str" type="string" gui-text="repeat_str:">indefinite</param>
<param name="dur_str" type="string" gui-text="dur_str:">9.1</param> <param name="dur_str" type="string" gui-text="dur_str:">9.1</param>
<param name="delete" type="bool" gui-text="Remove frames">true</param>
<effect> <effect>
<object-type>all</object-type> <object-type>all</object-type>
<effects-menu> <effects-menu>

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding=utf-8
# #
# Copyright (C) 2021 roberta bennett repeatingshadow@protonmail.com # Copyright (C) 2021 roberta bennett repeatingshadow@protonmail.com
# #
@ -18,15 +17,18 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# #
""" """
Create svg path animation from frames. Create svg path animation as SMIL from frames.
Place each frame of the animation in a layer named 'frame'. Place each frame of the animation in a layer named 'frame'.
Each of these layers should have the same number of paths, Each of these layers should have the same number of paths,
and each path should have the same number of points as the corresponding and each path should have the same number of points as the corresponding
path in other layers. path in other layers.
Note if there are more than one path in the frames, the Z order of the paths
must match. It helps to use the XML editor option to observe the z order.
The animation is applied to the paths in the first layer in the sequence, so the The animation is applied to the paths in the first layer in the sequence, so the
properties of that layer are used. properties of that layer are used. In particular, the first layer ought to be set
visible.
Animations with different numbers of frames can be put into different sequences, Animations with different numbers of frames can be put into different sequences,
named 'sequence', using sub-groups: named 'sequence', using sub-groups:
@ -51,9 +53,9 @@ Layers:
frame frame
frame frame
Layer names must contain 'frame' and groups names contain 'sequence',
eg, frame1 frame 2 frame 30, sequence 1, rythm sequence, rocket sequence
use layer named exactly 'frame' and groups named exactly 'sequence',
not, eg, frame1 frame2 frame3 !
""" """
@ -70,18 +72,14 @@ class AnimateElement(inkex.BaseElement):
class AnimationExtension(inkex.EffectExtension): class AnimationExtension(inkex.EffectExtension):
def add_arguments(self, pars): def add_arguments(self, pars):
pars.add_argument("--delete", type=inkex.Boolean, help="Remove frames")
pars.add_argument("--begin_str", default="0", help="begin string: eg 0;an2.end;an3.begin") pars.add_argument("--begin_str", default="0", help="begin string: eg 0;an2.end;an3.begin")
pars.add_argument("--repeat_str", default="indefinite", help="indefinite or an integer") pars.add_argument("--repeat_str", default="indefinite", help="indefinite or an integer")
pars.add_argument("--dur_str", default="7.9", help="duration in seconds. Do not decorate with units") pars.add_argument("--dur_str", default="7.9", help="duration in seconds. Do not decorate with units")
def effect(self): def crunchFrames(self,frames):
sequences = self.svg.findall("svg:g[@inkscape:label='sequence']") if frames is None:
if len(sequences) == 0:
raise inkex.AbortExtension("layer named sequence does not exist.")
for sequence in sequences:
frames = sequence.findall("svg:g[@inkscape:label='frame']")
if len(frames) == 0:
raise inkex.AbortExtension("layer named frame does not exist.") raise inkex.AbortExtension("layer named frame does not exist.")
frame0paths = frames[0].findall('svg:path') frame0paths = frames[0].findall('svg:path')
Dlists = [p.get_path() for p in frame0paths] Dlists = [p.get_path() for p in frame0paths]
@ -98,8 +96,28 @@ class AnimationExtension(inkex.EffectExtension):
repeatCount=self.options.repeat_str, repeatCount=self.options.repeat_str,
values=dl) values=dl)
frame0paths[i].append(animel) frame0paths[i].append(animel)
for frame in frames[1:]:
if self.options.delete:
frame.delete()
return
def effect(self):
sequences = [ elem for elem in self.svg.findall("svg:g[@inkscape:label]")
if "sequence" in (elem.get('inkscape:label'))
]
if len(sequences)==0:
frames = [ elem for elem in self.svg.findall("svg:g[@inkscape:label]")
if "frame" in (elem.get('inkscape:label'))
]
self.crunchFrames(frames)
return
for sequence in sequences:
frames = [ elem for elem in sequence.findall("svg:g[@inkscape:label]")
if "frame" in (elem.get('inkscape:label'))
]
self.crunchFrames(frames)
if __name__ == '__main__': if __name__ == '__main__':
AnimationExtension().run() AnimationExtension().run()

View File

@ -59,9 +59,8 @@
<option value="1016x711">1016 x 711 mm (Fusion M2 40)</option> <option value="1016x711">1016 x 711 mm (Fusion M2 40)</option>
<option value="1219x914">1219 x 914 mm (Fusion Pro 48)</option> <option value="1219x914">1219 x 914 mm (Fusion Pro 48)</option>
</param> </param>
<param name="max_cutting_speed" type="float" min="1.000" max="9999.000" precision="2" gui-text="Max. cutting speed (mm/s)">120</param> <param name="max_cutting_speed" type="float" min="1.000" max="9999.000" precision="2" gui-text="Max. cutting speed (mm/s)">120.0</param>
<param name="max_travel_speed" type="float" min="1.000" max="9999.000" precision="2" gui-text="Max. travel speed (mm/s)">500</param> <param name="max_travel_speed" type="float" min="1.000" max="9999.000" precision="2" gui-text="Max. travel speed (mm/s)">500.0</param>
<param name="cut_travel_factor" type="float" min="0.001" max="10.00" precision="3" gui-text="Factor between cutting/travel moves" gui-description="Usually ~60-80% are cutting time, rest is travel time. Set this factor to express the amount of travel time in relation to cutting time.">0.60</param>
<param name="price_per_minute_gross" type="float" min="0.0" max="9999.0" precision="2" gui-text="Price/minute € (gross)">2.0</param> <param name="price_per_minute_gross" type="float" min="0.0" max="9999.0" precision="2" gui-text="Price/minute € (gross)">2.0</param>
<param name="vector_grid_xy" type="float" min="0.0" max="9999.0" precision="2" gui-text="Vector grid (mm)">12.0</param> <param name="vector_grid_xy" type="float" min="0.0" max="9999.0" precision="2" gui-text="Vector grid (mm)">12.0</param>
</page> </page>

View File

@ -13,17 +13,16 @@ class LaserCheck(inkex.EffectExtension):
''' '''
ToDos: ToDos:
- check for elements which have the attribute "display:none" (some groups have this)
- inx: - inx:
- set speed manually or pick machine (epilog) - travel and cut speed are prefilled then - set speed manually or pick machine (epilog) - travel and cut speed are prefilled then
- calculate cut estimation with linear or non-linear (epiloog) speeds > select formula or like this - calculate cut estimation with linear or non-linear (epilog) speeds > select formula or like this
- select time estimation for specific speed percentage for for all speeds (100,90, ...) - select time estimation for specific speed percentage for for all speeds (100,90, ...)
- select material (parameters -> how to???) - select material (parameters -> how to???)
- select power of CO² source - select power of CO² source
- add fields for additional costs like configuring the machine or grabbing parts out of the machine (weeding), etc. - add fields for additional costs like configuring the machine or grabbing parts out of the machine (weeding), etc.
- add mode select: cut, engrave - add mode select: cut, engrave
- look for lines especially containing id "travelLines-" and sum up travel lines
- add some extra seconds for start, stop, removing parts, attaching material, ... - add some extra seconds for start, stop, removing parts, attaching material, ...
- maybe remove totalTravelLength and set manually ...
- Handlungsempfehlungen einbauen - Handlungsempfehlungen einbauen
- verweisen auf diverse plugins, die man nutzen kann: - verweisen auf diverse plugins, die man nutzen kann:
- migrate ungrouper - migrate ungrouper
@ -65,9 +64,8 @@ class LaserCheck(inkex.EffectExtension):
pars.add_argument('--tab') pars.add_argument('--tab')
pars.add_argument('--machine_size', default="812x508") pars.add_argument('--machine_size', default="812x508")
pars.add_argument('--max_cutting_speed', type=float, default=500) pars.add_argument('--max_cutting_speed', type=float, default=120)
pars.add_argument('--max_travel_speed', type=float, default=150) pars.add_argument('--max_travel_speed', type=float, default=500)
pars.add_argument('--cut_travel_factor', type=float, default=0.60)
pars.add_argument('--price_per_minute_gross', type=float, default=2.0) pars.add_argument('--price_per_minute_gross', type=float, default=2.0)
pars.add_argument('--vector_grid_xy', type=float, default=12.0) #TODO pars.add_argument('--vector_grid_xy', type=float, default=12.0) #TODO
@ -572,27 +570,36 @@ class LaserCheck(inkex.EffectExtension):
if so.checks == "check_all" or so.cutting_estimation is True: if so.checks == "check_all" or so.cutting_estimation is True:
inkex.utils.debug("\n---------- Cutting time estimation (Epilog Lasers)") inkex.utils.debug("\n---------- Cutting time estimation (Epilog Lasers)")
totalCuttingLength = 0 totalCuttingLength = 0
pathCount = 0 totalTravelLength = 0
cuttingPathCount = 0
travelPathCount = 0
for element in shapes: for element in shapes:
if isinstance(element, inkex.PathElement): if isinstance(element, inkex.PathElement):
slengths, stotal = csplength(element.path.transform(element.composed_transform()).to_superpath()) slengths, stotal = csplength(element.path.transform(element.composed_transform()).to_superpath())
if "-travelLine" in element.get('id'): #we use that id scheme together with the extension "Draw Directions / Travel Moves"
totalTravelLength += stotal
travelPathCount += 1
elif "markerId-" in element.get('id'):
pass #we skip the path "markerId-<nr>", possibly generated by the extension "Draw Directions / Travel Moves
else:
totalCuttingLength += stotal totalCuttingLength += stotal
pathCount += 1 #each path has one start and one end (if open path) or start=end on closed path. For cutting the we calculate with 2 points because we enter and leave each path ALWAYS cuttingPathCount += 1
totalTravelLength = totalCuttingLength * so.cut_travel_factor
totalLength = totalCuttingLength + totalTravelLength totalLength = totalCuttingLength + totalTravelLength
inkex.utils.debug("total paths={}".format(pathCount)) inkex.utils.debug("total cutting paths={}".format(cuttingPathCount))
inkex.utils.debug("total travel paths={}".format(travelPathCount))
inkex.utils.debug("(measured) cutting length (mm) = {:0.2f} mm".format(self.svg.uutounit(str(totalCuttingLength), "mm"), self.svg.uutounit(str(totalCuttingLength), "mm"))) inkex.utils.debug("(measured) cutting length (mm) = {:0.2f} mm".format(self.svg.uutounit(str(totalCuttingLength), "mm"), self.svg.uutounit(str(totalCuttingLength), "mm")))
inkex.utils.debug("(estimated) travel length (mm) = {:0.2f} mm".format(self.svg.uutounit(str(totalTravelLength), "mm"), self.svg.uutounit(str(totalTravelLength), "mm"))) inkex.utils.debug("(measured) travel length (mm) = {:0.2f} mm".format(self.svg.uutounit(str(totalTravelLength), "mm"), self.svg.uutounit(str(totalTravelLength), "mm")))
inkex.utils.debug("(estimated) total length (mm) = {:0.2f} mm".format(self.svg.uutounit(str(totalLength), "mm"), self.svg.uutounit(str(totalLength), "mm"))) inkex.utils.debug("(measured) total length (mm) = {:0.2f} mm".format(self.svg.uutounit(str(totalLength), "mm"), self.svg.uutounit(str(totalLength), "mm")))
''' from https://www.epiloglaser.com/assets/downloads/fusion-material-settings.pdf ''' from https://www.epiloglaser.com/assets/downloads/fusion-material-settings.pdf
Speed Settings: The speed setting scale of 1% to 100% is not linear "Speed Settings: The speed setting scale of 1% to 100% is not linear
i.e. 100% speed will not be twice as fast as 50% speed. This non-linear i.e. 100% speed will not be twice as fast as 50% speed. This non-linear
scale is very useful in compensating for the different factors that affect engraving time. scale is very useful in compensating for the different factors that affect engraving time."
''' '''
for speedFactor in [100,90,80,70,60,50,40,30,20,10,9,8,7,6,5,4,3,2,1]: for speedFactor in [100,90,80,70,60,50,40,30,20,10,9,8,7,6,5,4,3,2,1]:
speedFactorR = speedFactor / 100.0 speedFactorR = speedFactor / 100.0
adjusted_speed = 482.4000 / so.max_cutting_speed #empiric - found out by trying for hours ... adjusted_speed = 480.0 / so.max_cutting_speed #empiric - found out by trying for hours ...
empiric_scale = 1 + (speedFactorR**2) / 19.0 #empiric - found out by trying for hours ... empiric_scale = 1 + (speedFactorR**2) / 15.25 #empiric - found out by trying for hours ...
v_cut = so.max_cutting_speed * speedFactorR v_cut = so.max_cutting_speed * speedFactorR
v_travel = so.max_travel_speed #this is always at maximum v_travel = so.max_travel_speed #this is always at maximum
tsec_cut = (self.svg.uutounit(str(totalCuttingLength)) / (adjusted_speed * so.max_cutting_speed * speedFactorR)) * empiric_scale tsec_cut = (self.svg.uutounit(str(totalCuttingLength)) / (adjusted_speed * so.max_cutting_speed * speedFactorR)) * empiric_scale

View File

@ -0,0 +1,20 @@
[
{
"name": "Open Extension Directory",
"id": "fablabchemnitz.de.open_dir",
"path": "open_dir",
"dependent_extensions": null,
"original_name": "Open Extension Directory",
"original_id": "fablabchemnitz.de.open_dir",
"license": "GNU GPL v3",
"license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/LICENSE",
"comment": "Written by Mario Voigt",
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/open_dir",
"fork_url": null,
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Open+Extension+Directory",
"inkscape_gallery_url": null,
"main_authors": [
"github.com/vmario89"
]
}
]

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Open Extension Directory</name>
<id>fablabchemnitz.de.open_dir</id>
<effect needs-live-preview="false">
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz"/>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">open_dir.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
import subprocess
import os
import sys
import warnings
import inkex
DETACHED_PROCESS = 0x00000008
class OpenExtensionDirectory(inkex.EffectExtension):
def spawnIndependentProcess(self, args):
warnings.simplefilter('ignore', ResourceWarning) #suppress "enable tracemalloc to get the object allocation traceback"
if os.name == 'nt':
subprocess.Popen(args, close_fds=True, creationflags=DETACHED_PROCESS)
else:
subprocess.Popen(args, start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
warnings.simplefilter("default", ResourceWarning)
def effect(self):
extension_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', '..')
if os.name == 'nt':
explorer = "explorer"
else:
explorer = "xdg-open"
args = [explorer, extension_dir]
try:
self.spawnIndependentProcess(args)
except FileNotFoundError as e:
inkex.utils.debug(e)
exit(1)
if __name__ == '__main__':
OpenExtensionDirectory().run()

View File

@ -307,7 +307,7 @@ class RobotBoxes(inkex.EffectExtension):
# Translate group # Translate group
#transform = 'translate(' + str( self.svg.namedview.center[0] ) + ',' + str( self.svg.namedview.center[1] ) + ')' #transform = 'translate(' + str( self.svg.namedview.center[0] ) + ',' + str( self.svg.namedview.center[1] ) + ')'
g = etree.SubElement(self.svg.get_current_layer(), 'g', {inkex.addNS('label','inkscape'):'RobotBox'}) g = etree.SubElement(self.svg.get_current_layer(), 'g', {inkex.addNS('label','inkscape'):'RobotBox'})
g.transform = transform #g.transform = transform
# Create SVG Path for box bounds # Create SVG Path for box bounds
style = { 'stroke': '#000000', 'fill': 'none', 'stroke-width':str(self.svg.unittouu("1px")) } style = { 'stroke': '#000000', 'fill': 'none', 'stroke-width':str(self.svg.unittouu("1px")) }