"""
ImportGCode, and Inkscape extension by Nathaniel Klumb

This extension adds support for some GCode files to the File/Import...
dialog in Inkscape.  It loads the GCode file passed to it by Inkscape as a
command-line parameter and writes the resulting SVG to stdout (which is how
Inkscape input plugins work).

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.
"""
import re
import inkex
from StringIO import StringIO
from math import sqrt,pi,sin,cos,tan,acos,atan2,fabs

class ImportGCode:
    """ Import a GCode file and process it into an SVG. """
    current_id = 0
    geometry_error = False
    
    def __init__(self,gcode_filename,v_carve=False,laser_mode=False,
                 ignore_z=True,label_z=True,
                 tool_diameter=1.0,v_angle=90.0,v_top=0.0,v_step=1.0):
        """ Load a GCode file and process it into an SVG. """
        self.unit = 1.0
        self.ignore_z = ignore_z or v_carve
        self.label_z = label_z and not v_carve
        self.tool_diameter = tool_diameter
        self.v_carve = v_carve
        self.v_angle = v_angle * pi / 180.0
        self.v_top = v_top
        self.v_step = v_step
        self.laser_mode = laser_mode
        self.spindle = False
        self.speed = 0
        with open(gcode_filename) as file:
            self.loadGCode(file)
        self.createSVG()

    def getIJ(self,x1,y1,x2,y2,r):
        """ Calculate I and J from two arc endpoints and a radius. """
        theta = atan2(y2 - y1, x2 - x1)
        alpha = acos(sqrt((x2 - x1)**2 + (y2 - y1)**2)/(2 * abs(r)))
        return (r * cos(theta + alpha), r * sin(theta + alpha))

    def getTangentPoints(self,x1,y1,r1,x2,y2,r2):
        """ Compute the four outer tangent endpoints of two circles. """
        theta = atan2(y2 - y1, x2 - x1)
        try:
            alpha = acos((r1 - r2)/sqrt((x2 - x1)**2 + (y2 - y1)**2))
        except ValueError:
            # It's broken, but we'll just cap it off.
            # The SVG will be messed up, but that's better feedback
            # than just blankly saying, "Sorry, please try again."
            if not self.geometry_error: #Only show the error once.
                inkex.errormsg('Math error importing V-carve: '
                               'V-bit angle too large?')
                inkex.errormsg('    Check your included angle '
                               'setting and try again.')
                self.geometry_error = True
            if ((r1 - r2)/sqrt((x2 - x1)**2 + (y2 - y1)**2) < -1):
                alpha = pi
            else:
                alpha = 0
        return ((x1 + r1 * cos(theta - alpha), y1 + r1 * sin(theta - alpha)),
                (x1 + r1 * cos(theta + alpha), y1 + r1 * sin(theta + alpha)),
                (x2 + r2 * cos(theta + alpha), y2 + r2 * sin(theta + alpha)),
                (x2 + r2 * cos(theta - alpha), y2 + r2 * sin(theta - alpha)))

    def intersectLines(self, p1, p2, p3, p4): 
        """ Calculate the intersection of Line(p1,p2) and Line(p3,p4)
                
        returns a tuple: (x, y, valid, included)
            (x, y): the intersection
            valid: a unique solution exists
            included: the solution is within both the line segments
                                Segment(p1,p2) and Segment(p3,p4)
        """

        DET_TOLERANCE = 0.00000001
        T = 0.00000001
        
        # the first line is pt1 + r*(p2-p1)
        x1,y1 = p1
        x2,y2 = p2
        dx1 = x2 - x1
        dy1 = y2 - y1

        # the second line is p4 + s*(p4-p3)
        x3,y3 = p3
        x4,y4 = p4;
        dx2 = x4 - x3
        dy2 = y4 - y3

        # In matrix form:
        #        [ dx1  -dx2 ][ r ] = [ x3-x1 ]
        #        [ dy1  -dy2 ][ s ] = [ y3-y1 ]
        #
        # Which can be solved:
        #        [ r ] = _1_  [ -dy2  dx2 ] [ x3-x1 ]
        #        [ s ] = DET  [ -dy1  dx1 ] [ y3-y1 ]
        #
        # With the deteminant: DET = (-dx1 * dy2 + dy1 * dx2)
        DET = (-dx1 * dy2 + dy1 * dx2)

        # If DET is zero, they're parallel
        if fabs(DET) < DET_TOLERANCE:
            # If they overlap, either p3 or p4 must be
            # an included point, so check one, then check the
            # other.  If either falls inside the segment from
            # p1 to p2, return it as *a* valid intersection.
            # Otherwise, return the bad news -- no intersection.
            #
            # Also, when checking the limits, allow a tolerance, T,
            # since we're working in floating point.
            if ((((x3 >= x1 - T) and (x3 <= x2 + T)) or 
                 ((x3 <= x1 + T) and (x3 >= x2 - T))) and
                (((y3 >= y1 - T) and (y3 <= y2 + T)) or 
                ((y3 <= y1 + T) and (y3 >= y2 - T)))):
                return (x3,y3,False,True)
            elif ((((x4 >= x1 - T) and (x4 <= x2 + T)) or 
                   ((x4 <= x1 + T) and (x4 >= x2 - T))) and
                  (((y4 >= y1 - T) and (y4 <= y2 + T)) or 
                   ((y4 <= y1 + T) and (y4 >= y2 - T)))):
                return (x4,y4,False,True)
            # NO CONNECTION... *dialtone*
            else:
                return (None,None,False,False)
                
        # Since the determinant is non-zero, now take the reciprocal.
        invDET = 1.0/DET

        # We want to calculate the intersection for each line so we can
        # average the results together.  They should be identical, but
        # floating-point and rounding error, etc...       
        # Calculate the scalar distances along Line(p1,p2) and Line(p3,p4)
        r = invDET * (-dy2 * (x3-x1) + dx2 * (y3-y1))
        s = invDET * (-dy1 * (x3-x1) + dx1 * (y3-y1))

        # Average the intersection's coordinates from the two lines.
        x = (x1 + r*dx1 + x3 + s*dx2)/2.0
        y = (y1 + r*dy1 + y3 + s*dy2)/2.0
        
        # Now one last check to see if the intersection's coordinates are
        # included within both line segments.
        included = ((((x >= x1 - T) and (x <= x2 + T)) or 
                     ((x <= x1 + T) and (x >= x2 - T))) and
                    (((y >= y1 - T) and (y <= y2 + T)) or 
                     ((y <= y1 + T) and (y >= y2 - T))) and
                    (((x >= x3 - T) and (x <= x4 + T)) or 
                     ((x <= x3 + T) and (x >= x4 - T))) and
                    (((y >= y3 - T) and (y <= y4 + T)) or 
                     ((y <= y3 + T) and (y >= y4 - T))))
        return (x,y,True,included)
    
    def getRadius(self,Z):
        """ Compute the radius of a V-bit given a Z coordinate. 
        
        If the V-bit is above stock top, we just mirror it.
        Technically, the file's broken, but hey, may as well do something.
        """
        if (self.v_top <= Z):
            return (Z - self.v_top) * tan(self.v_angle / 2)
        else:
            return (self.v_top - Z) * tan(self.v_angle / 2)
    
    def getAngle(self,center,point):
        """ Calculate the angle from a center to a point. """
        a = atan2(point[1] - center[1], point[0] - center[0])
        return a + ((2*pi) if (a<0.0) else 0)
    
    def isLargeAngle(self,center,p1,p2):
        """ Determine whether the SVG large angle flag should be set. """
        a1 = self.getAngle(center,p1)
        a2 = self.getAngle(center,p2)
        angle = a1 - a2
        if angle < 0:
            angle += 2 * pi
        return 1 if (abs(angle) > pi) else 0

    def interpolatePoints(self,center,p1,p2):
        """ Interpolate a set of points along an arc. """
        a1 = self.getAngle(center,p1)
        a2 = self.getAngle(center,p2)
        angle = a2 - a1
        dz = 1.0 * (p2[2]-p1[2])
        r = sqrt((center[0] - p1[0])**2 + (center[1] - p1[1])**2)
        length = r * abs(angle) / pi
        steps = int(round(length/self.v_step))
        points = []
        for i in range(1,steps):
          point = (center[0] + r * cos(a1 + angle*i/steps),
                   center[1] + r * sin(a1 + angle*i/steps),
                   p1[2] + dz*i/steps)
          points += [point]
        points += [p2]
        return points
    
    def makeVcarve(self,v_segments):
        """ Connect multiple V-carve segments into one path. 
        
        Start on one V-carve segment and chain all the way to the
        opposite end, then add a switchback and chain all the way
        back to the beginning.
        """
        vs = v_segments
        # Move to the starting point.
        path = 'M {} {} '.format(vs[0][1][0][0],vs[0][1][0][1])
        # Initial arc, if it's not a point.
        if vs[0][0][0][2] > 0:
            path += ('A {} {} 0 {} {} {} {} '
                    ).format(vs[0][0][0][2],vs[0][0][0][2],
                                 1 if (vs[0][0][0][2] > vs[0][0][1][2]) else 0,
                                 0,vs[0][1][1][0],vs[0][1][1][1])
        # Step through all the segments on the way to the other end.
        for v in range(len(vs)-1):
            # Check whether an intersection exists between the two
            # line segments.  If so, use it, otherwise, connect with an arc.
            x,y,valid,included = self.intersectLines(vs[v][1][1],
                                                     vs[v][1][2],
                                                     vs[v+1][1][1],
                                                     vs[v+1][1][2])
            if included: #line segments
                path += 'L {} {} '.format(x,y)
            else:
                path += ('L {} {} A {} {} 0 {} {} {} {} '
                        ).format(vs[v][1][2][0],vs[v][1][2][1],
                                     vs[v][0][1][2],vs[v][0][1][2],
                                     self.isLargeAngle(vs[v][0][1],
                                                       vs[v][1][2],
                                                       vs[v+1][1][1]),
                                     0,vs[v+1][1][1][0],vs[v+1][1][1][1])
        # Connecting line.
        path += 'L {} {} '.format(vs[len(vs)-1][1][2][0],
                                  vs[len(vs)-1][1][2][1])
        # Switchback arc, if it's not a point.
        if vs[len(vs)-1][0][1][2] > 0:
            path += ('A {} {} 0 {} {} {} {} '
                    ).format(vs[len(vs)-1][0][1][2],vs[len(vs)-1][0][1][2],
                             1 if (vs[len(vs)-1][0][1][2] >
                                   vs[len(vs)-2][0][0][2]) else 0,
                             0,vs[len(vs)-1][1][3][0],vs[len(vs)-1][1][3][1])
        # Step through all the segments on the way back home.
        for v in range(len(vs)-1,0,-1):
            # Check whether an intersection exists between the two
            # line segments.  If so, use it, otherwise, connect with an arc.
            x,y,valid,included = self.intersectLines(vs[v][1][3],
                                                     vs[v][1][0],
                                                     vs[v-1][1][3],
                                                     vs[v-1][1][0])
            if included: #line segments
                path += 'L {} {} '.format(x,y)
            else:
                path += ('L {} {} A {} {} 0 {} {} {} {} '
                        ).format(vs[v][1][0][0],vs[v][1][0][1],
                                 vs[v-1][0][1][2],vs[v-1][0][1][2],
                                 self.isLargeAngle(vs[v-1][0][1],
                                                   vs[v][1][0],
                                                   vs[v-1][1][3]),
                                 0,vs[v-1][1][3][0],vs[v-1][1][3][1])
        # And finally, close the curve.
        path += 'Z'
        return path
            
    def getVsegment(self,x1,y1,z1,x2,y2,z2):
        """ Compute the required data to define a V-carve segment. """
        r1 = self.getRadius(z1)
        r2 = self.getRadius(z2)
        p = self.getTangentPoints(x1,y1,r1,x2,y2,r2)
        return (((x1,y1,r1),(x2,y2,r2)),p)
        
    def parseLine(self,command,X,Y,Z,line,no_path=False):
        """ Parse a line of G-code. 
        
        This takes the current coordinates and modal command, then processes
        the new line of G-code to yield a new ending set of coordinates
        plus values necessary for curve computations.  It also returns the
        resulting path data, unless otherwise indicated, e.g. for V-carves.
        """
        comments = re.compile('\([^\)]*\)')
        commands = re.compile('([MSGXYZIJKR])([-.0-9]+)')
        
        lastX = X
        lastY = Y
        lastZ = Z
        I = 0.0
        J = 0.0
        K = 0.0
        R = None
        results = commands.findall(comments.sub('',line))
        for (code,val) in results:
            v = float(val)
            i = int(v)
            
            if code == 'M':
                if i == 3:
                    self.spindle = True
                elif i == 5:
                    self.spindle = False
            elif code == 'S':
                self.speed = v
            elif code == 'G':
                if i == 0:
                    command = 'G0'
                elif i == 1:
                    command = 'G1'
                elif i == 2:
                    command = 'G2'
                elif i == 3:
                    command = 'G3'
                elif i == 20:
                    self.unit = 25.4
                elif i == 21:
                    self.unit = 1.0
                elif val == "90":
                    self.absolute = True
                elif val == "91":
                    self.absolute = False
                elif val == "90.1":
                    self.absoluteIJK = True
                elif val == "91.1":
                    self.absoluteIJK = False
            elif code == 'X':
                if self.absolute:
                    X = v * self.unit
                else:
                    X += v * self.unit
            elif code == 'Y':
                if self.absolute:
                    Y = v * self.unit
                else:
                    Y += v * self.unit
            elif code == 'Z':
                if self.absolute:
                    Z = v * self.unit
                else:
                    Z += v * self.unit
            elif code == 'I':
                I = v * self.unit
                if self.absoluteIJK:
                    I -= X
            elif code == 'J':
                J = v * self.unit
                if self.absoluteIJK:
                    J -= Y
            elif code == 'K':
                # Sure, process it, but we don't *do* anything with K.
                K = v * self.unit
                if self.absoluteIJK:
                    K -= Z
            elif code == 'R':
                R = v * self.unit

        if no_path: # V-carving doesn't need any path data.
            return ((command, X, Y, Z, I, J, K, R, ''))
            
        # The line's been parsed.  Now let's generate path data from it.
        path = ''
        if command == 'G1':
            # If there's any XY motion, make a line segment.
            if ((X != lastX) or (Y != lastY)):
                path = 'L {} {} '.format(round(X,5),round(Y,5))
        elif (command == 'G2') or (command == 'G3'):
            # Arcs! Oh, what glorious fun we'll have!  First, sweep direction.
            sweep = 0 if (command == 'G2') else 1
            # R overrules I and J if both are present, so we compute
            # new I and J values based on R.  We need those to determine
            # whether the Large Angle Flag needs to be set.
            if R is not None:
                I,J = self.getIJ(lastX,lastY,X,Y,R)
            if (I != 0.0) or (J != 0.0):
                if sweep == 0:
                    large_arc = self.isLargeAngle((lastX+I,lastY+J),
                                                  (lastX,lastY),(X,Y))
                else:
                    large_arc = self.isLargeAngle((lastX+I,lastY+J),
                                                  (X,Y),(lastX,lastY))
                radius = sqrt(I**2 + J**2)
                path = 'A {} {} 0 {} {} {} {} '.format(round(radius,5),
                                                       round(radius,5),
                                                       large_arc,sweep,
                                                       round(X,5),round(Y,5))
            # No R, and no I or J either?  Let's just call it a line segment.
            # (It may have had a K, but we don't believe in K for SVG imports.)
            else:
                path = 'L {} {} '.format(round(X,5),round(Y,5))

        # In laser mode, if the spindle isn't active or the speed is zero,
        # there's no lasing to be had.  Drop the path data.  (The Inkscape
        # extension from J Tech Photonics uses G1/G2/G3 moves throughout,
        # with nary a G0, so if we don't do this, we'll show unlasered paths.)
        if (self.laser_mode and ((not self.spindle) or (self.speed == 0))):
            path = ''
        return ((command, X, Y, Z, I, J, K, R, path))

    def savePath(self,path,Z):
        """ Save a set of path data, filing it by Z if appropriate. """
        if (path.find('A') == -1) and (path.find('L') == -1):
            return #empty path
        if self.ignore_z:
            if path not in self.paths:
                self.paths.add(path)
        else:
            try:
                if path not in self.paths_by_z[Z]:
                    self.paths_by_z[Z].add(path)
            except KeyError:
                self.paths_by_z[Z] = set([path])
        
    def loadGCode(self,gcode_file):
        """ Load a G-code file, handling the contents. """
        if self.ignore_z:
            self.paths = set([])
        else:
            self.paths_by_z = {}
        self.absolute = True
        self.absoluteIJK = False
        self.unit=1.0
        command = ''
        X = 0.0
        Y = 0.0
        Z = 0.0
        lastX = X
        lastY = Y
        lastZ = Z
        self.minX = 0.0
        self.minY = 0.0
        self.minZ = 0.0
        self.maxX = 0.0
        self.maxY = 0.0
        self.maxZ = 0.0
        
        path = ''
        line = gcode_file.readline()
        v_segments = []
        while line:
            command,X,Y,Z,I,J,K,R,path_data = self.parseLine(command, 
                                                             X, Y, Z, line,
                                                             self.v_carve)
            self.minX = X if X < self.minX else self.minX
            self.maxX = X if X > self.maxX else self.maxX
            self.minY = Y if Y < self.minY else self.minY
            self.maxY = Y if Y > self.maxY else self.maxY
            self.minZ = Z if Z < self.minZ else self.minZ
            self.maxZ = Z if Z > self.maxZ else self.maxZ

            # V-carve mode.
            if self.v_carve:
                if (lastX != X) or (lastY != Y):
                    if command == 'G1':
                        v_segments += [self.getVsegment(lastX, lastY, lastZ,
                                                        X, Y, Z)]
                    elif (command == 'G2') or (command == 'G3'):
                        # We don't attempt to handle the plethora of curves
                        # that can result from V-carving arcs.  Instead, we
                        # just interpolate them and process the subparts.
                        points = self.interpolatePoints((lastX+I,lastY+J),
                                                        (lastX,lastY,lastZ),
                                                        (X,Y,Z))
                        iX = lastX
                        iY = lastY
                        iZ = lastZ
                        for p in points:
                            v_segments += [self.getVsegment(iX, iY, iZ,
                                                            p[0], p[1], p[2])]
                            iX = p[0]
                            iY = p[1]
                            iZ = p[2]
                    else:
                        if len(v_segments):
                            self.savePath(self.makeVcarve(v_segments),'VCarve')
                        v_segments = []
            # Standard mode (non-V-carve).
            else:
                if ((command == 'G0') or 
                    (not self.ignore_z and (Z != lastZ)) or 
                    (self.laser_mode and ((not self.spindle) or 
                                          (self.speed == 0)))):
                    if (path != ''):
                        self.savePath(path,lastZ)
                        path = ''
                if (((command == 'G1') or 
                     (command == 'G2') or 
                     (command == 'G3')) and (path == '')):
                    path = 'M {} {} {}'.format(lastX,lastY,path_data)
                else:
                    path += path_data
            lastX = X
            lastY = Y
            lastZ = Z
            line = gcode_file.readline()
        # Always remember to save the tail end of your work.
        if self.v_carve:
            if len(v_segments):
                self.savePath(self.makeVcarve(v_segments),'VCarve')
        else:
            if (path != ''):
                self.savePath(path,lastZ)
            
    def filterPaths(self):
        """ Filter out duplicate paths, leaving only the deepest instance. """
        if self.ignore_z:
            return
        z_depths = sorted(self.paths_by_z,None,None,True)
        for i in range(0,len(z_depths)):
            for j in range(i+1, len(z_depths)):
                self.paths_by_z[z_depths[i]] -= self.paths_by_z[z_depths[j]]
    
    def next_id(self):
        """ Return an incrementing value. """
        self.current_id += 1
        return self.current_id
    
    def getStyle(self,color='#000000',width=None):
        """ Create a CSS-type style string. """
        if width is None:
            width = self.tool_diameter
        return ('opacity:1;vector-effect:none;fill:none;fill-opacity:1;'
                'stroke:{};stroke-width:{};stroke-opacity:1;'
                'stroke-linecap:round;stroke-linejoin:round;'
                'stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0'
               ).format(color,width)
    
    def createSVG(self):
        """ Create the output SVG. """
        base = ('<svg xmlns="http://www.w3.org/2000/svg"'
                ' width="{}mm" height="{}mm" viewBox="{} {} {} {}"/>'
               ).format(self.maxX-self.minX, self.maxY-self.minY,
                        self.minX, self.minY, self.maxX-self.minX, self.maxY-self.minY)
        self.doc = inkex.etree.parse(StringIO((base)))
        svg = self.doc.getroot()
        # Since G-code and SVG interpret Y in opposite directions,
        # we just group everything under a transform that mirrors Y.
        svg = inkex.etree.SubElement(svg,'g',{'id':'gcode',
                                              'transform':'scale(1,-1)'})
        # Add illustrative axes to the SVG to facilitate positioning.
        inkex.etree.SubElement(svg,'path',
                               {'d':'M 0 {} V {}'.format(self.minY, self.maxY),
                                'style':self.getStyle('#00ff00',0.5),
                                'id':'vertical'})
        inkex.etree.SubElement(svg,'path',
                               {'d':'M {} 0 H {}'.format(self.minX, self.maxX),
                                'style':self.getStyle('#ff0000',0.5),
                                'id':'horizontal'})
        # For V-carves, include the paths and use a narrow stroke width.
        if self.v_carve:
            for path in self.paths:
                inkex.etree.SubElement(svg,'path',
                                       {'d':path,
                                        'style':self.getStyle(width=0.1),
                                        'id':'path{}'.format(self.next_id())})
        # For standard mode with Z ignored, include the paths.
        elif self.ignore_z:
            for path in self.paths:
                inkex.etree.SubElement(svg,'path',
                                       {'d':path,
                                        'style':self.getStyle(),
                                         'id':'path{}'.format(self.next_id())})
        # For standard mode with Z grouping, filter the paths, 
        # then add each group of paths (and optionally, labels).
        else:
            self.filterPaths()
            z_depths = sorted(self.paths_by_z)
            depth_num = 0
            for i in range(0,len(z_depths)):
                if len(self.paths_by_z[z_depths[i]]):
                    params = {'id':('group{}-{}'
                                   ).format(self.next_id(),z_depths[i]),
                              'style':self.getStyle()}
                    group = inkex.etree.SubElement(svg,'g',params)
                    
                    # If labels are enabled, add the label to the group.
                    if self.label_z:
                        params = {'x':'{}'.format(self.maxX),
                                  'y':'{}'.format(depth_num*-5),
                                  'transform':'scale(1,-1)',
                                  'id':'text{}'.format(i),
                                  'style':('opacity:1;fill:#0000ff;'
                                           'fill-opacity:1;stroke:none;'
                                           'font-size:4.5')}
                        if self.unit == 1.0:
                          label = '{} mm'.format(z_depths[i])
                        else:
                          label = '{} in'.format(z_depths[i]/self.unit)
                        inkex.etree.SubElement(group,'text',params
                                              ).text = label
                                     
                        depth_num += 1

                    # Now add the paths to the group.
                    for path in self.paths_by_z[z_depths[i]]:
                        id = 'path{}'.format(self.next_id())
                        inkex.etree.SubElement(group,'path',
                                               {'d':path,
                                                'style':self.getStyle(),
                                                'id':id})
            # If labels are enabled, label the labels.
            if self.label_z:
                inkex.etree.SubElement(svg,'text',
                                       {'x':'{}'.format(self.maxX),
                                        'y':'{}'.format(depth_num*-5),
                                        'transform':'scale(1,-1)',
                                        'id':'text{}'.format(i),
                                        'style':('opacity:1;fill:#0000ff;'
                                                 'fill-opacity:1;stroke:none;'
                                                 'font-size:4.5')}
                                      ).text = 'Z Groups:'

# And now for the code to allow Inkscape to run our lovely extension.                                      
if __name__ == '__main__':
    parser = inkex.optparse.OptionParser(usage=('usage: %prog [options]'
                                                ' GCodeFile'),
                                         option_class=inkex.InkOption)
    # Mode select:
    parser.add_option('-m', '--mode', action='store', 
                      help='Mode: vcarve, standard, laser',
                      default='standard')
    # V-carve options:
    parser.add_option('-a', '--v_angle', action='store',
                      help='Included (full) angle for V-bit, in degrees.',
                      default=None)
    parser.add_option('-t', '--v_top', action='store',
                      help='Stock top (usually zero)', default=None)
    parser.add_option('-s', '--v_step', action='store',
                      help='Step size for curve interpolation.', default=None)
    # Standard options:
    parser.add_option('-d', '--tool_diameter', action='store',
                      help='Tool diameter / path width.', default=None)
    # General options:
    parser.add_option('-u', '--units', action='store',
                      help='Dialog units.', default='mm')
    parser.add_option('-z', '--z_axis', action='store',
                      help='Z-axis: ignore,group,label', default=False)
    # Non-options to handle .inx "parameters":
    parser.add_option('--tab', action='store')
    parser.add_option('--inputhelp', action='store')
    
    # Now, process, my lovelies!
    (options, args) = parser.parse_args(inkex.sys.argv[1:])
    
    # First steps first, what mode?
    v_carve = False
    ignore_z = False
    laser_mode = False
    if (options.mode == 'vcarve'):
      v_carve = True
    elif (options.mode == 'laser'):
      laser_mode = True
    
    # V-carve parameters.    
    try:
        v_angle = round(float(options.v_angle),3)
    except ValueError:
        v_angle = 1.0
    try:
        v_top = round(float(options.v_top) * 
                      (25.4 if (options.units == 'in') else 1.0),5)
    except ValueError:
        v_top = 0.0
    try:
        v_step = round(float(options.v_step) * 
                       (25.4 if (options.units == 'in') else 1.0),5)
    except ValueError:
        v_step = 1.0
    
    # Standard parameters.    
    try:
        diameter = round(float(options.tool_diameter) * 
                         (25.4 if (options.units == 'in') else 1.0),3)
    except ValueError:
        diameter = 1.0
    
    # General options.
    ignore_z = (options.z_axis == 'ignore')
    label_z = (options.z_axis == 'label')
     
    gc = ImportGCode(args[0], v_carve, laser_mode, ignore_z, label_z,
                     diameter, v_angle, v_top, v_step)
    gc.doc.write(inkex.sys.stdout)
