#!/usr/bin/env python 
# coding=utf-8
'''
Copyright (C) 2008 Aaron Spike, aaron@ekips.org
Overcut, Tool Offset, Rotation, Serial Com., Many Bugfixes and Improvements: Sebastian Wüst, sebi@timewaster.de, http://www.timewasters-place.com/

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
'''
import inkex, simpletransform, cubicsuperpath, simplestyle, sys, math, bezmisc, string, cspsubdiv

class MyEffect(inkex.Effect):
    def __init__(self):
        inkex.Effect.__init__(self)
        self.OptionParser.add_option("-f", "--flatness",
                        action="store", type="float", 
                        dest="flat", default=1.2,
                        help="Minimum flatness of the subdivided curves")
        self.OptionParser.add_option("-m", "--mirror",
                        action="store", type="inkbool", 
                        dest="mirror", default="FALSE",
                        help="Mirror Y-Axis")
        self.OptionParser.add_option("-v", "--center",
                        action="store", type="inkbool", 
                        dest="center", default="FALSE",
                        help="Center Zero Point")
        self.OptionParser.add_option("-x", "--xOffset",
                        action="store", type="float", 
                        dest="xOffset", default=0.0,
                        help="X offset (mm)")
        self.OptionParser.add_option("-y", "--yOffset",
                        action="store", type="float", 
                        dest="yOffset", default=0.0,
                        help="Y offset (mm)")
        self.OptionParser.add_option("-r", "--resolution",
                        action="store", type="int", 
                        dest="resolution", default=1016,
                        help="Resolution (dpi)")
        self.OptionParser.add_option("-n", "--pen",
                        action="store", type="int",
                        dest="pen", default=1,
                        help="Pen number")
        self.OptionParser.add_option("-p", "--plotInvisibleLayers",
                        action="store", type="inkbool", 
                        dest="plotInvisibleLayers", default="FALSE",
                        help="Plot invisible layers")
        self.OptionParser.add_option("-u", "--useOvercut",
                        action="store", type="inkbool", 
                        dest="useOvercut", default="TRUE",
                        help="Use Overcut")
        self.OptionParser.add_option("-o", "--overcut",
                        action="store", type="float", 
                        dest="overcut", default=1.0,
                        help="Overcut (mm)")
        self.OptionParser.add_option("-c", "--correctToolOffset",
                        action="store", type="inkbool", 
                        dest="correctToolOffset", default="TRUE",
                        help="Correct tool offset")
        self.OptionParser.add_option("-t", "--toolOffset",
                        action="store", type="float", 
                        dest="toolOffset", default=0.25,
                        help="Tool offset (mm)")
        self.OptionParser.add_option("-w", "--toolOffsetReturn",
                        action="store", type="float", 
                        dest="toolOffsetReturn", default=2.5,
                        help="Return Factor")
        self.OptionParser.add_option("-a", "--angle",
                        action="store", type="string", 
                        dest="rotation", default="90",
                        help="Orientation")
        self.OptionParser.add_option("-s", "--sendToPlotter",
                        action="store", type="inkbool", 
                        dest="sendToPlotter", default="FALSE",
                        help="Send to Plotter also")
        self.OptionParser.add_option("-z", "--port",
                        action="store", type="string", 
                        dest="port", default="COM1",
                        help="Serial Port")
        self.OptionParser.add_option("-b", "--baudRate",
                        action="store", type="string", 
                        dest="baudRate", default="9600",
                        help="Baud Rate")
 
    def effect(self):
        # initiate vars
        self.xDivergence = 9999999999999.0
        self.yDivergence = 9999999999999.0
        self.xSize = -9999999999999.0
        self.ySize = -9999999999999.0
        scale = float(self.options.resolution) / 90 # dots/inch to dots/pixels
        x0 = self.options.xOffset * 3.5433070866 * scale # mm to dots
        y0 = self.options.yOffset * 3.5433070866 * scale # mm to dots
        self.options.overcut = self.options.overcut * 3.5433070866 * scale # mm to dots
        self.options.toolOffset = self.options.toolOffset * 3.5433070866 * scale # mm to dots
        self.options.flat = float(self.options.resolution) * self.options.flat / 1000
        doc = self.document.getroot()
        mirror = 1.0
        if self.options.mirror:
            mirror = -1.0
        # get viewBox parameter to correct scaling
        viewBox = doc.get('viewBox')
        viewBoxTransformX = 1
        viewBoxTransformY = 1
        if viewBox:
            viewBox = string.split(viewBox, ' ')
            if viewBox[2] and viewBox[3]:
                viewBoxTransformX = float(inkex.unittouu(doc.get('width'))) / float(viewBox[2])
                viewBoxTransformY = float(inkex.unittouu(doc.get('height'))) / float(viewBox[3])
        # dryRun to find edges
        self.dryRun = True
        self.groupmat = [[[scale*viewBoxTransformX, 0.0, 0.0], [0.0, mirror*scale*viewBoxTransformY, 0.0]]]
        self.groupmat[0] = simpletransform.composeTransform(self.groupmat[0], simpletransform.parseTransform('rotate(' + self.options.rotation + ')'))
        self.vData = [['', -1.0, -1.0], ['', -1.0, -1.0], ['', -1.0, -1.0], ['', -1.0, -1.0]]
        self.process_group(doc)
        if self.options.center:
            self.xDivergence += (self.xSize - self.xDivergence) / 2
            self.yDivergence += (self.ySize - self.yDivergence) / 2
        # life run
        self.dryRun = False
        self.groupmat = [[[scale*viewBoxTransformX, 0.0, -self.xDivergence + x0], [0.0, mirror*scale*viewBoxTransformY, -self.yDivergence + y0]]]
        self.groupmat[0] = simpletransform.composeTransform(self.groupmat[0], simpletransform.parseTransform('rotate(' + self.options.rotation + ')'))
        self.vData = [['', -1.0, -1.0], ['', -1.0, -1.0], ['', -1.0, -1.0], ['', -1.0, -1.0]]
        self.hpgl = 'IN;SP%d;' % self.options.pen
        if self.options.sendToPlotter:
            try:
                import serial
            except ImportError, e:
                inkex.errormsg('pySerial is not installed.\n\n1. Download pySerial here: http://pypi.python.org/pypi/pyserial\n2. Extract the "serial" folder from the zip to the following folder: "C:\Program Files (x86)\inkscape\python\Lib"\n3. Restart Inkscape.')
                self.options.sendToPlotter = False
            if self.options.sendToPlotter:          
                self.S = serial.Serial(port=self.options.port, baudrate=self.options.baudRate, timeout=0.01, writeTimeout=None)
                self.S.write('IN;SP%d;' % self.options.pen)
        self.process_group(doc)
        self.calcOffset('PU', 0, 0)
        self.hpgl += 'PU0,0;'
        if self.options.sendToPlotter:
            self.S.write('PU0,0;')
            self.S.read(2)
            self.S.close()

    def process_group(self, group):
        # process groups
        style = group.get('style')
        if style:
            style = simplestyle.parseStyle(style)
            if style.has_key('display'):
                if style['display'] == 'none':
                    if not self.options.plotInvisibleLayers:
                        return
        trans = group.get('transform')
        if trans:
            self.groupmat.append(simpletransform.composeTransform(self.groupmat[-1], simpletransform.parseTransform(trans)))
        for node in group:
            if node.tag == inkex.addNS('path','svg'):
                self.process_path(node, self.groupmat[-1])
            if node.tag == inkex.addNS('g','svg'):
                self.process_group(node)
        if trans:
            self.groupmat.pop()

    def process_path(self, node, mat):
        # process oath 
        d = node.get('d')
        if d:
            # transform path
            p = cubicsuperpath.parsePath(d)
            trans = node.get('transform')
            if trans:
                mat = simpletransform.composeTransform(mat, simpletransform.parseTransform(trans))
            simpletransform.applyTransformToPath(mat, p)
            cspsubdiv.cspsubdiv(p, self.options.flat)
            # break path into HPGL commands
            xPosOld = -1
            yPosOld = -1
            for sp in p:
                cmd = 'PU'
                for csp in sp:
                    xPos = csp[1][0]
                    yPos = csp[1][1]
                    if int(xPos) != int(xPosOld) or int(yPos) != int(yPosOld): 
                        self.calcOffset(cmd, xPos, yPos)
                        cmd = 'PD'
                        xPosOld = xPos
                        yPosOld = yPos
                # perform overcut
                if self.options.useOvercut:
                    if int(xPos) == int(sp[0][1][0]) and int(yPos) == int(sp[0][1][1]):
                        for csp in sp:
                            xPos2 = csp[1][0]
                            yPos2 = csp[1][1]
                            if int(xPos) != int(xPos2) or int(yPos) != int(yPos2):
                                self.calcOffset(cmd, xPos2, yPos2)
                                if self.options.overcut - self.getLength(xPosOld, yPosOld, xPos2, yPos2) <= 0:
                                    break                                      
                                xPos = xPos2
                                yPos = yPos2

    def getLength(self, x1, y1, x2, y2, abs = True):
        # calc absoulute or relative length of two points
        if abs: return math.fabs(math.sqrt((x2 - x1) ** 2.0 + (y2 - y1) ** 2.0))
        else: return math.sqrt((x2 - x1) ** 2.0 + (y2 - y1) ** 2.0)

    def getAlpha(self, x1, y1, x2, y2, x3, y3):
        temp1 = (x1-x2)**2 + (y1-y2)**2 + (x3-x2)**2 + (y3-y2)**2 - (x1-x3)**2 - (y1-y3)**2
        temp2 = 2 * math.sqrt((x1-x2)**2 + (y1-y2)**2) * math.sqrt((x3-x2)**2 + (y3-y2)**2)
        temp3 = temp1 / temp2
        if temp3 < -1:
            temp3 = -1
        return math.acos(temp3)

    def calcOffset(self, cmd, xPos, yPos):
        # calculate offset correction (or dont))
        if not self.options.correctToolOffset:
            self.storeData(cmd, xPos, yPos)
        else:
            self.vData.pop(0)
            self.vData.insert(3, [cmd, xPos, yPos])
            if self.vData[2][1] != -1.0:
                if self.vData[1][1] != -1.0:
                    if self.vData[2][0] == 'PD' and self.vData[3][0] == 'PD':
                        if self.getAlpha(self.vData[1][1], self.vData[1][2], self.vData[2][1], self.vData[2][2], self.vData[3][1], self.vData[3][2]) > 2.748893:
                            self.storeData(self.vData[2][0], self.vData[2][1], self.vData[2][2])
                            return
                    if self.vData[2][0] == 'PD':
                        self.storeData('PD', 
                            self.changeLengthX(self.vData[1][1], self.vData[1][2], self.vData[2][1], self.vData[2][2], self.options.toolOffset), 
                            self.changeLengthY(self.vData[1][1], self.vData[1][2], self.vData[2][1], self.vData[2][2], self.options.toolOffset))
                    elif self.vData[0][1] != -1.0:
                        self.storeData('PU', 
                            self.vData[2][1] - (self.vData[1][1] - self.changeLengthX(self.vData[0][1], self.vData[0][2], self.vData[1][1], self.vData[1][2], self.options.toolOffset)), 
                            self.vData[2][2] - (self.vData[1][2] - self.changeLengthY(self.vData[0][1], self.vData[0][2], self.vData[1][1], self.vData[1][2], self.options.toolOffset)))
                    else:
                        self.storeData('PU', 
                            self.vData[2][1], 
                            self.vData[2][2])
                    if self.vData[3][0] == 'PD':
                        self.storeData('PD', 
                            self.changeLengthX(self.vData[3][1], self.vData[3][2], self.vData[2][1], self.vData[2][2], -(self.options.toolOffset * self.options.toolOffsetReturn)), 
                            self.changeLengthY(self.vData[3][1], self.vData[3][2], self.vData[2][1], self.vData[2][2], -(self.options.toolOffset * self.options.toolOffsetReturn)))
                else:
                    self.storeData(self.vData[2][0], self.vData[2][1], self.vData[2][2])

    def storeData(self, command, x, y):
        if self.dryRun:
            if x < self.xDivergence: self.xDivergence = x 
            if y < self.yDivergence: self.yDivergence = y
            if x > self.xSize: self.xSize = x
            if y > self.ySize: self.ySize = y
        else:
            if not self.options.center:
                if x < 0: x = 0 # only positive values are allowed
                if y < 0: y = 0
            self.hpgl += '%s%d,%d;' % (command, x, y)
            if self.options.sendToPlotter:
                self.S.write('%s%d,%d;' % (command, x, y))

    def changeLengthX(self, x1, y1, x2, y2, offset):
        # change length between two points - x axis
        if offset < 0: offset = max(-self.getLength(x1, y1, x2, y2), offset)
        return x2 + (x2 - x1) / self.getLength(x1, y1, x2, y2, False) * offset;

    def changeLengthY(self, x1, y1, x2, y2, offset):
        # change length between two points - y axis
        if offset < 0: offset = max(-self.getLength(x1, y1, x2, y2), offset)
        return y2 + (y2 - y1) / self.getLength(x1, y1, x2, y2, False) * offset;

    def output(self):
        # print to file
        print self.hpgl

if __name__ == '__main__':
    # Raise recursion limit to avoid exceptions on big documents
    sys.setrecursionlimit(20000);
    # start calculations
    e = MyEffect()
    e.affect()

# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99
