#!/usr/bin/env python ''' Copyright (C) 2009 glen.harris@middlegable.org based on gcode.py (C) 2007 hugomatic... based on dots.py (C) 2005 Aaron Spike, aaron@ekips.org This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Note that this still needs to be extended in the following ways: 1) Convert path curves internally into straight lines and/or arcs. This is what the machineTolerance parameter is designed for. ''' import inkex, simplestyle, simplepath import os, math, copy, re, sys import gettext _ = gettext.gettext def nolog(text): pass logDebug = nolog #logDebug = inkex.debug logError = inkex.debug logMessage = inkex.debug logWarning = inkex.debug g_svgNamespace = "http://www.w3.org/2000/svg" g_inkspaceNamespace = "http://www.inkscape.org/namespaces/inkscape" g_toolPath = "ToolPath" option_filenameBase = "" option_machineTolerance = 0.005 option_traverseZ = 1.0 option_defaultCutZ = -1.0 option_defaultFeedRate = 1.0 option_returnToXYOrigin = True eps = 0.00001 def makeSvgTag(i_tag): return "{" + g_svgNamespace + "}" + i_tag def makeInkspaceTag(i_tag): return "{" + g_inkspaceNamespace + "}" + i_tag class Point(object): def __init__(self, x=0.0, y=0.0): self.x = x self.y = y def normalise(self): length = self.magnitude() self.x /= length self.y /= length def magnitude(self): return math.sqrt(self.x*self.x + self.y*self.y) def translate(self, offset): self.x += offset.x self.y += offset.y def __str__(self): return "[%.3f,%.3f]" % (self.x, self.y) class StraightLineTo(Point): def __init__(self, point): self.x = point.x self.y = point.y class CubicLineTo(Point): def __init__(self, p1, p2, point): self.p1 = p1 self.p2 = p2 self.x = point.x self.y = point.y class LineSegmentPath(object): def __init__(self): self.isClosed = False self.points = [] self.name = None self.feedRate = None self.toolSelection = None self.cutZ = None def getCutZ(self, default): if self.cutZ is None: return default return self.cutZ def getFeedRate(self, default): if self.feedRate is None: return default return self.feedRate def isStarted(self): if len(self.points) > 0: return True return False def startAtPoint(self, point): assert len(self.points) == 0 assert not self.isClosed self.points.append(point) def straightLineToPoint(self, point): assert len(self.points) > 0 assert not self.isClosed self.points.append(StraightLineTo( point )) def cubicLineToPoint(self, p1, p2, point): assert len(self.points) > 0 assert not self.isClosed self.points.append(CubicLineTo( p1, p2, point) ) def startAtCoords(self, x, y): self.startAtPoint(Point(x, y)) def straightLineToCoords(self, x, y): self.straightLineToPoint(Point(x, y)) def cubicLineToCoords(self, p1x, p1y, p2x, p2y, x, y): self.cubicLineToPoint(Point(x, y)) def close(self): assert not self.isClosed self.isClosed = True # Check to make sure the start and end points aren't in the same spot start = self.points[0] end = self.points[-1] if (start.x == end.x ) and (start.y==end.y): self.points[-1:] = [] class LineSegment(object): def __init__(self, start, end): self.start = start self.end = end def __str__(self): return "%s-%s" % (self.start, self.end) def offset(self, amount ): direction = Point( self.end.x - self.start.x, self.end.y - self.start.y ) logDebug(direction) direction.normalise() logDebug(direction) offset = Point( direction.y * amount, -direction.x * amount) self.start.translate( offset ); self.end.translate( offset ); logDebug("%s %s" % (self.start,self.end)) class GcodeOperationList(object): def __init__(self): self.lines = [] def appendInstruction(self, code, **arguments): line = code + " " argnames = arguments.keys() argnames.sort() for name in argnames : line += "%c%.4f " % (name[0].upper(),arguments[name]) self.lines.append( line ) def appendToolChange(self, tool): self.lines.append("%s M6" % tool) def appendEnd(self): self.lines.append("M2") def appendRapidMove(self, **arguments): self.appendInstruction("G00", **arguments) def appendFeedMove(self,**arguments): self.appendInstruction("G01",**arguments) def appendComment(self, comment): self.lines.append( "(" + comment + ")" ) def setUnitsToMillimeters(self): self.lines.append("G21 (All units in mm)") def setUnitsToInches(self): self.lines.append("G20 (All units in inches)") def calculateBounds(path): point = path.points[0] minX = maxX = point.x minY = maxY = point.y for point in path.points[1:]: minX = min(point.x, minX) minY = min(point.y, minY) maxX = max(point.x, maxX) maxY = max(point.y, maxY) return LineSegment( Point(minX,minY), Point(maxX,maxY)) def convertFromSvgPath( svgPath, scale ): anyInvalidPathSegments = False d = svgPath.attrib.get('d') p = simplepath.parsePath(d) newPath = LineSegmentPath() # Loop through each element of the line, building up a path representation for cmd, params in p: if cmd == 'M': # Moveto - should be beginning of path if not newPath.isStarted(): x = float(params[0])/scale y = float(params[1])/scale newPath.startAtCoords(x, y) else: logWarning(_("Path contains multiple sub paths and will be ignored")) return None elif cmd == 'L': # Lineto - somewhere inside the path x = float(params[0])/scale y = float(params[1])/scale newPath.straightLineToCoords(x, y) elif cmd == 'Cxxx': # Cubicto - p1, p2, p3 anyInvalidPathSegments = True p1x = float(params[0])/scale p1y = float(params[1])/scale p2x = float(params[2])/scale p2y = float(params[3])/scale x = float(params[4])/scale y = float(params[5])/scale newPath.cubicLineToCoords(p1x, p1y, p2x, p2y, x, y) elif cmd == 'Z': newPath.close() else: logWarning(_("Path contains non-straight line segments and will be ignored")) return None return newPath class ExportGcode(inkex.Effect): def __init__(self): inkex.Effect.__init__(self) self.OptionParser.add_option("-x", "--tab", action="store", type="string", dest="tab", default=None) self.OptionParser.add_option("--filenameBase", action="store", type="string", dest="filenameBase", default=option_filenameBase, help="Base filename (layer names will be appended)") self.OptionParser.add_option("--machineTolerance", action="store", type="string", dest="machineTolerance", default=option_machineTolerance, help="Machine tolerance/resolution (in drawing units)") self.OptionParser.add_option("--traverseZ", action="store", type="float", dest="traverseZ", default=option_traverseZ, help="Height to traverse when not cutting (in drawing units)") self.OptionParser.add_option("--defaultCutZ", action="store", type="float", dest="defaultCutZ", default=option_defaultCutZ, help="Default height to cut at (in drawing units)") self.OptionParser.add_option("--defaultFeedRate", action="store", type="float", dest="defaultFeedRate", default=option_defaultFeedRate, help="Default tool feed rate (in drawing units per minute)") self.OptionParser.add_option("--returnToXYOrigin", action="store", type="inkbool", dest="returnToXYOrigin", default=option_returnToXYOrigin, help="Return to X0.0 Y0.0 at end") def effect(self): logDebug("Args %s" % sys.argv) # Determine units for the document: unitAttr = self.document.xpath('//sodipodi:namedview/@inkscape:document-units', namespaces=inkex.NSS) units = "mm" if unitAttr: units = unitAttr[0] logMessage("Units: %s" % units ) #Calculate conversion factor from drawing units to physical units try: conversion = inkex.uuconv[units] except KeyError: logError("Invalid units - preserving scale") conversion = 1.0 foundValidPath = False paths = [] for id, selectedElement in self.selected.iteritems(): if selectedElement.tag == makeSvgTag("path"): newPath = convertFromSvgPath( selectedElement, conversion ) if newPath is not None: paths.append(newPath) if len(paths) > 0: foundValidPath = True self.writeMilledPaths( paths, units, "SelectedObjects" ) else: toolre = re.compile(r"\s+([FTZ])(-?[0-9]*\.?[0-9]*)") # Check if there are any groups named 'ToolPath.*' for node in self.document.xpath('//svg:g', namespaces=inkex.NSS): layerName = node.attrib.get(makeInkspaceTag("label")) if layerName[:len(g_toolPath)]==g_toolPath: paths = [] # Check to see if there are feed rates, toolpaths or cut depths specified for path in node.xpath('svg:path', namespaces=inkex.NSS): newPath = convertFromSvgPath( path, conversion ) if newPath is not None: parameters = toolre.findall(layerName) logDebug(parameters) for parameter in parameters: if parameter[0]=="F": newPath.feedRate = float(parameter[1]) elif parameter[0]=="T": newPath.toolSelection = "T" + parameter[1] elif parameter[0]=="Z": newPath.cutZ = float(parameter[1]) paths.append(newPath) if len(paths) > 0: foundValidPath = True self.writeMilledPaths(paths, units, layerName) if not foundValidPath: logError(_("No suitable (straight line) paths were selected")) def writeMilledPaths( self, paths, units, name): assert len(paths) > 0 mops = GcodeOperationList() mops.appendComment("Found %i paths for %s:" % (len(paths), name) ) if units == "mm": mops.setUnitsToMillimeters() elif units == "in": mops.setUnitsToInches() else: #leave units undefined logWarning("Invalid units '%s'" % units ) for path in paths: description = self.describePath(path, "Path") for line in description: mops.appendComment( line ) logMessage( line ) self.millPath(path, mops) mops.appendEnd() #Check if we are writing to a file or not logDebug("About to write output") if len(self.options.filenameBase) > 0: filename = self.options.filenameBase if not re.match(r'.*[\\/]$', filename): filename += " " # # Munge any bad characters out of the layer info filename += re.sub(r'[^a-zA-Z0-9]',"", name) self.writeMops(mops, filename + ".nc") else: # Being run as 'SaveAs', so just output to stdout for line in mops.lines: print line def millPath(self, path, mops): mops.appendRapidMove(z=self.options.traverseZ) if path.toolSelection is not None: mops.appendToolChange( path.toolSelection ) point = path.points[0] mops.appendRapidMove(x=point.x, y=point.y) mops.appendFeedMove(z=path.getCutZ(self.options.defaultCutZ), f=path.getFeedRate(self.options.defaultFeedRate)) for point in path.points[1:]: #we now have a line segment that goes from firstPoint to secondPoint mops.appendFeedMove(x=point.x, y=point.y) if path.isClosed: point = path.points[0] mops.appendFeedMove(x=point.x, y=point.y) mops.appendRapidMove(z=self.options.traverseZ) if self.options.returnToXYOrigin: mops.appendRapidMove(x=0.0, y=0.0) def describePath(self, path, name, showPoints=False): description = [] bounds = calculateBounds( path ) description.append(name + " has size %.3f x %.3f" % (bounds.end.x - bounds.start.x, bounds.end.y -bounds.start.y) ) description.append(name + " has bounding box %.3f-%.3f wide by %.3f-%.3f high" % (bounds.start.x, bounds.end.x, bounds.start.y, bounds.end.y) ) if showPoints: for point in path.points: description.append("Point " % point ) return description def writeMops(self, mops, filename): logDebug("Writing to %s" % filename) ngcfile = open(filename, 'wb') for line in mops.lines: ngcfile.write(line) ngcfile.write("\r\n") ngcfile.close() def output(self): if len(self.options.filenameBase) > 0: # Run as an effect, so output, otherwise do nothing (run as output) inkex.Effect.output(self) effect = ExportGcode() effect.affect()