hpgl_encoder.py revision 895c4d1b4a298259c78b62e1b43842093e300591
6194N/A#!/usr/bin/env python
6194N/A# coding=utf-8
6194N/A'''
6194N/ACopyright (C) 2008 Aaron Spike, aaron@ekips.org
6194N/ACopyright (C) 2013 Sebastian Wüst, sebi@timewaster.de
6194N/A
6194N/AThis program is free software; you can redistribute it and/or modify
6194N/Ait under the terms of the GNU General Public License as published by
6194N/Athe Free Software Foundation; either version 2 of the License, or
6194N/A(at your option) any later version.
6194N/A
6194N/AThis program is distributed in the hope that it will be useful,
6194N/Abut WITHOUT ANY WARRANTY; without even the implied warranty of
6194N/AMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
6194N/AGNU General Public License for more details.
6194N/A
6194N/AYou should have received a copy of the GNU General Public License
6194N/Aalong with this program; if not, write to the Free Software
6194N/AFoundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
6194N/A'''
6194N/A
6194N/A# standard libraries
6194N/Aimport math
6194N/Aimport re
6194N/Aimport string
6194N/A#from StringIO import StringIO
6194N/A# local libraries
6194N/Aimport bezmisc
6194N/Aimport cspsubdiv
6194N/Aimport cubicsuperpath
6194N/Aimport inkex
6194N/Aimport simplestyle
6194N/Aimport simpletransform
6194N/A
6194N/A
6194N/Aclass hpglEncoder:
6194N/A PI = math.pi
6194N/A TWO_PI = PI * 2
6194N/A
6194N/A def __init__(self, effect):
6194N/A ''' options:
6194N/A "resolutionX":float
6194N/A "resolutionY":float
6194N/A "pen":int
6194N/A "force:int
6194N/A "speed:int
6194N/A "orientation":string // "0", "90", "-90", "180"
6194N/A "mirrorX":bool
6194N/A "mirrorY":bool
6194N/A "center":bool
6194N/A "flat":float
6194N/A "overcut":float
6194N/A "toolOffset":float
6194N/A "precut":bool
6194N/A "autoAlign":bool
6194N/A "debug":bool
6194N/A '''
6194N/A self.options = effect.options
6194N/A self.doc = effect.document.getroot()
6194N/A self.docWidth = effect.unittouu(self.doc.get('width'))
6194N/A self.docHeight = effect.unittouu(self.doc.get('height'))
6194N/A self.divergenceX = 'False'
6194N/A self.divergenceY = 'False'
6194N/A self.sizeX = 'False'
6194N/A self.sizeY = 'False'
6194N/A self.dryRun = True
6194N/A self.lastPoint = [0, 0, 0]
6194N/A self.offsetX = 0
6194N/A self.offsetY = 0
6194N/A self.scaleX = self.options.resolutionX / effect.unittouu("1.0in") # dots per inch to dots per user unit
6194N/A self.scaleY = self.options.resolutionY / effect.unittouu("1.0in") # dots per inch to dots per user unit
6194N/A scaleXY = (self.scaleX + self.scaleY) / 2
6194N/A self.overcut = effect.unittouu(str(self.options.overcut) + "mm") * scaleXY # mm to dots (plotter coordinate system)
6194N/A self.toolOffset = effect.unittouu(str(self.options.toolOffset) + "mm") * scaleXY # mm to dots
6194N/A self.flat = self.options.flat / (1016 / ((self.options.resolutionX + self.options.resolutionY) / 2)) # scale flatness to resolution
6194N/A if self.toolOffset > 0.0:
6194N/A self.toolOffsetFlat = self.flat / self.toolOffset * 4.5 # scale flatness to offset
6194N/A else:
6194N/A self.toolOffsetFlat = 0.0
6194N/A self.mirrorX = 1.0
6194N/A if self.options.mirrorX:
6194N/A self.mirrorX = -1.0
6194N/A self.mirrorY = -1.0
6194N/A if self.options.mirrorY:
6194N/A self.mirrorY = 1.0
6194N/A if self.options.debug:
6194N/A self.debugValues = {}
6194N/A self.debugValues['docWidth'] = self.docWidth
6194N/A self.debugValues['docHeight'] = self.docHeight
6194N/A # process viewBox attribute to correct page scaling
6194N/A self.viewBoxTransformX = 1
6194N/A self.viewBoxTransformY = 1
6194N/A if self.options.debug:
6194N/A self.debugValues['viewBoxWidth'] = "-"
6194N/A self.debugValues['viewBoxHeight'] = "-"
6194N/A viewBox = self.doc.get('viewBox')
6194N/A if viewBox:
6194N/A viewBox2 = string.split(viewBox, ',')
6194N/A if len(viewBox2) < 4:
6194N/A viewBox2 = string.split(viewBox, ' ')
6194N/A if self.options.debug:
6194N/A self.debugValues['viewBoxWidth'] = viewBox2[2]
6194N/A self.debugValues['viewBoxHeight'] = viewBox2[3]
6194N/A self.viewBoxTransformX = self.docWidth / effect.unittouu(effect.addDocumentUnit(viewBox2[2]))
6194N/A self.viewBoxTransformY = self.docHeight / effect.unittouu(effect.addDocumentUnit(viewBox2[3]))
6194N/A
6194N/A def getHpgl(self):
6194N/A # dryRun to find edges
6194N/A groupmat = [[self.mirrorX * self.scaleX * self.viewBoxTransformX, 0.0, 0.0], [0.0, self.mirrorY * self.scaleY * self.viewBoxTransformY, 0.0]]
6194N/A groupmat = simpletransform.composeTransform(groupmat, simpletransform.parseTransform('rotate(' + self.options.orientation + ')'))
6194N/A self.vData = [['', -1.0, -1.0], ['', -1.0, -1.0], ['', -1.0, -1.0], ['', -1.0, -1.0]]
6194N/A self.processGroups(self.doc, groupmat)
6194N/A if self.divergenceX == 'False' or self.divergenceY == 'False' or self.sizeX == 'False' or self.sizeY == 'False':
6194N/A raise Exception('NO_PATHS')
6194N/A # live run
6194N/A self.dryRun = False
6194N/A if self.options.debug:
6194N/A self.debugValues['drawingWidth'] = self.sizeX - self.divergenceX
6194N/A self.debugValues['drawingHeight'] = self.sizeY - self.divergenceY
6194N/A self.debugValues['drawingWidthUU'] = self.debugValues['drawingWidth'] / self.scaleX
6194N/A self.debugValues['drawingHeightUU'] = self.debugValues['drawingHeight'] / self.scaleY
6194N/A # move drawing according to various modifiers
6194N/A if self.options.autoAlign:
6194N/A if self.options.center:
6194N/A self.offsetX -= (self.sizeX - self.divergenceX) / 2
6194N/A self.offsetY -= (self.sizeY - self.divergenceY) / 2
6194N/A else:
6194N/A self.divergenceX = 0.0
6194N/A self.divergenceY = 0.0
6194N/A if self.options.center:
6194N/A if self.options.orientation == '0':
6194N/A self.offsetX -= (self.docWidth * self.scaleX) / 2
6194N/A self.offsetY += (self.docHeight * self.scaleY) / 2
6194N/A if self.options.orientation == '90':
6194N/A self.offsetY += (self.docWidth * self.scaleX) / 2
6194N/A self.offsetX += (self.docHeight * self.scaleY) / 2
6194N/A if self.options.orientation == '180':
6194N/A self.offsetX += (self.docWidth * self.scaleX) / 2
6194N/A self.offsetY -= (self.docHeight * self.scaleY) / 2
6194N/A if self.options.orientation == '270':
6194N/A self.offsetY -= (self.docWidth * self.scaleX) / 2
6194N/A self.offsetX -= (self.docHeight * self.scaleY) / 2
6194N/A else:
6194N/A if self.options.orientation == '0':
6194N/A self.offsetY += self.docHeight * self.scaleY
6194N/A if self.options.orientation == '90':
6194N/A self.offsetY += self.docWidth * self.scaleX
6194N/A self.offsetX += self.docHeight * self.scaleY
6194N/A if self.options.orientation == '180':
6194N/A self.offsetX += self.docWidth * self.scaleX
6194N/A if not self.options.center and self.toolOffset > 0.0:
6194N/A self.offsetX += self.toolOffset
6194N/A self.offsetY += self.toolOffset
6194N/A # initialize transformation matrix and cache
6194N/A groupmat = [[self.mirrorX * self.scaleX * self.viewBoxTransformX, 0.0, -self.divergenceX + self.offsetX],
6194N/A [0.0, self.mirrorY * self.scaleY * self.viewBoxTransformY, -self.divergenceY + self.offsetY]]
6194N/A groupmat = simpletransform.composeTransform(groupmat, simpletransform.parseTransform('rotate(' + self.options.orientation + ')'))
6194N/A self.vData = [['', -1.0, -1.0], ['', -1.0, -1.0], ['', -1.0, -1.0], ['', -1.0, -1.0]]
6194N/A # store initial hpgl commands
6194N/A self.hpgl = 'IN;SP%d' % self.options.pen
6194N/A if self.options.force > 0:
6194N/A self.hpgl += ';FS%d' % self.options.force
6194N/A if self.options.speed > 0:
6194N/A self.hpgl += ';VS%d' % self.options.speed
6194N/A # add move to zero point and precut
6194N/A if self.toolOffset > 0.0 and self.options.precut:
6194N/A if self.options.center:
6194N/A # position precut outside of drawing plus one times the tooloffset
6194N/A if self.offsetX >= 0.0:
6194N/A precutX = self.offsetX + self.toolOffset
6194N/A else:
6194N/A precutX = self.offsetX - self.toolOffset
6194N/A if self.offsetY >= 0.0:
6194N/A precutY = self.offsetY + self.toolOffset
6194N/A else:
6194N/A precutY = self.offsetY - self.toolOffset
6194N/A self.processOffset('PU', precutX, precutY)
6194N/A self.processOffset('PD', precutX, precutY + self.toolOffset * 8)
6194N/A else:
6194N/A self.processOffset('PU', 0, 0)
6194N/A self.processOffset('PD', 0, self.toolOffset * 8)
6194N/A else:
6194N/A self.processOffset('PU', 0, 0)
6194N/A # start conversion
6194N/A self.processGroups(self.doc, groupmat)
6194N/A # shift an empty node in in order to process last node in cache
6194N/A self.processOffset('PU', 0, 0)
6194N/A # add return to zero point
6194N/A self.hpgl += ';PU0,0;'
6194N/A if self.options.debug:
6194N/A return self.hpgl, self
6194N/A else:
6194N/A return self.hpgl, ""
6194N/A
6194N/A def processGroups(self, doc, groupmat):
6194N/A # flatten layers and groups to avoid recursion
6194N/A paths = []
6194N/A for node in doc:
6194N/A if (node.tag == inkex.addNS('g', 'svg') and self.isGroupVisible(node)) or node.tag == inkex.addNS('path', 'svg'):
6194N/A paths.append([node.tag, node, self.mergeTransform(node, groupmat)])
6194N/A doc = ''
6194N/A hasGroups = True
6194N/A while hasGroups:
6194N/A hasGroups = False
6194N/A for i, elm in enumerate(paths):
6194N/A if paths[i][0] == inkex.addNS('g', 'svg') and self.isGroupVisible(paths[i][1]):
6194N/A hasGroups = True
6194N/A for path in paths[i][1]:
6194N/A if (path.tag == inkex.addNS('g', 'svg') and self.isGroupVisible(path)) or path.tag == inkex.addNS('path', 'svg'):
6194N/A paths.append([path.tag, path, self.mergeTransform(path, paths[i][2])])
6194N/A paths[i][0] = ''
6194N/A for node in paths:
6194N/A if node[0] == inkex.addNS('path', 'svg'):
6194N/A self.processPath(node[1], node[2])
6194N/A
6194N/A def mergeTransform(self, doc, matrix):
6194N/A # get and merge two matrixes into one
6194N/A trans = doc.get('transform')
6194N/A if trans:
6194N/A return simpletransform.composeTransform(matrix, simpletransform.parseTransform(trans))
6194N/A else:
6194N/A return matrix
6194N/A
6194N/A def isGroupVisible(self, group):
6194N/A style = group.get('style')
6194N/A if style:
6194N/A style = simplestyle.parseStyle(style)
6194N/A if 'display' in style and style['display'] == 'none':
6194N/A return False
6194N/A return True
6194N/A
6194N/A def processPath(self, node, mat):
6194N/A # process path
6194N/A path = node.get('d')
6194N/A if path:
6194N/A # parse and transform path
6194N/A path = cubicsuperpath.parsePath(path)
6194N/A simpletransform.applyTransformToPath(mat, path)
6194N/A cspsubdiv.cspsubdiv(path, self.flat)
6194N/A # path to HPGL commands
6194N/A oldPosX = 0.0
6194N/A oldPosY = 0.0
6194N/A for singlePath in path:
6194N/A cmd = 'PU'
6194N/A for singlePathPoint in singlePath:
6194N/A posX, posY = singlePathPoint[1]
6194N/A # check if point is repeating, if so, ignore
6194N/A if int(round(posX)) != int(round(oldPosX)) or int(round(posY)) != int(round(oldPosY)):
6194N/A self.processOffset(cmd, posX, posY)
6194N/A cmd = 'PD'
6194N/A oldPosX = posX
6194N/A oldPosY = posY
6194N/A # perform overcut
6194N/A if self.overcut > 0.0 and not self.dryRun:
6194N/A # check if last and first points are the same, otherwise the path is not closed and no overcut can be performed
6194N/A if int(round(oldPosX)) == int(round(singlePath[0][1][0])) and int(round(oldPosY)) == int(round(singlePath[0][1][1])):
6194N/A overcutLength = 0
6194N/A for singlePathPoint in singlePath:
6194N/A posX, posY = singlePathPoint[1]
6194N/A # check if point is repeating, if so, ignore
6194N/A if int(round(posX)) != int(round(oldPosX)) or int(round(posY)) != int(round(oldPosY)):
6194N/A overcutLength += self.getLength(oldPosX, oldPosY, posX, posY)
6194N/A if overcutLength >= self.overcut:
6194N/A newLength = self.changeLength(oldPosX, oldPosY, posX, posY, - (overcutLength - self.overcut))
6194N/A self.processOffset(cmd, newLength[0], newLength[1])
6194N/A break
6194N/A else:
6194N/A self.processOffset(cmd, posX, posY)
6194N/A oldPosX = posX
6194N/A oldPosY = posY
6194N/A
6194N/A def getLength(self, x1, y1, x2, y2, absolute=True):
6194N/A # calc absoulute or relative length between two points
6194N/A length = math.sqrt((x2 - x1) ** 2.0 + (y2 - y1) ** 2.0)
6194N/A if absolute:
6194N/A length = math.fabs(length)
6194N/A return length
6194N/A
6194N/A def changeLength(self, x1, y1, x2, y2, offset):
6194N/A # change length of line
6194N/A if offset < 0:
6194N/A offset = max( - self.getLength(x1, y1, x2, y2), offset)
6194N/A x = x2 + (x2 - x1) / self.getLength(x1, y1, x2, y2, False) * offset
6194N/A y = y2 + (y2 - y1) / self.getLength(x1, y1, x2, y2, False) * offset
6194N/A return [x, y]
6194N/A
6194N/A def processOffset(self, cmd, posX, posY):
6194N/A # calculate offset correction (or dont)
6194N/A if self.toolOffset == 0.0 or self.dryRun:
6194N/A self.storePoint(cmd, posX, posY)
6194N/A else:
6194N/A # insert data into cache
6194N/A self.vData.pop(0)
6194N/A self.vData.insert(3, [cmd, posX, posY])
6194N/A # decide if enough data is availabe
6194N/A if self.vData[2][1] != -1.0:
6194N/A if self.vData[1][1] == -1.0:
6194N/A self.storePoint(self.vData[2][0], self.vData[2][1], self.vData[2][2])
6194N/A else:
6194N/A # perform tool offset correction (It's a *tad* complicated, if you want to understand it draw the data as lines on paper)
6194N/A if self.vData[2][0] == 'PD': # If the 3rd entry in the cache is a pen down command make the line longer by the tool offset
6194N/A pointThree = self.changeLength(self.vData[1][1], self.vData[1][2], self.vData[2][1], self.vData[2][2], self.toolOffset)
6194N/A self.storePoint('PD', pointThree[0], pointThree[1])
6194N/A elif self.vData[0][1] != -1.0:
6194N/A # Elif the 1st entry in the cache is filled with data and the 3rd entry is a pen up command shift
6194N/A # the 3rd entry by the current tool offset position according to the 2nd command
6194N/A pointThree = self.changeLength(self.vData[0][1], self.vData[0][2], self.vData[1][1], self.vData[1][2], self.toolOffset)
6194N/A pointThree[0] = self.vData[2][1] - (self.vData[1][1] - pointThree[0])
6194N/A pointThree[1] = self.vData[2][2] - (self.vData[1][2] - pointThree[1])
6194N/A self.storePoint('PU', pointThree[0], pointThree[1])
6194N/A else:
6194N/A # Else just write the 3rd entry
6194N/A pointThree = [self.vData[2][1], self.vData[2][2]]
6194N/A self.storePoint('PU', pointThree[0], pointThree[1])
6194N/A if self.vData[3][0] == 'PD':
6194N/A # If the 4th entry in the cache is a pen down command guide tool to next line with a circle between the prolonged 3rd and 4th entry
6194N/A if self.getLength(self.vData[2][1], self.vData[2][2], self.vData[3][1], self.vData[3][2]) >= self.toolOffset:
6194N/A pointFour = self.changeLength(self.vData[3][1], self.vData[3][2], self.vData[2][1], self.vData[2][2], - self.toolOffset)
6194N/A else:
6194N/A pointFour = self.changeLength(self.vData[2][1], self.vData[2][2], self.vData[3][1], self.vData[3][2],
6194N/A (self.toolOffset - self.getLength(self.vData[2][1], self.vData[2][2], self.vData[3][1], self.vData[3][2])))
6194N/A # get angle start and angle vector
6194N/A angleStart = math.atan2(pointThree[1] - self.vData[2][2], pointThree[0] - self.vData[2][1])
6194N/A angleVector = math.atan2(pointFour[1] - self.vData[2][2], pointFour[0] - self.vData[2][1]) - angleStart
6194N/A # switch direction when arc is bigger than 180°
6194N/A if angleVector > self.PI:
6194N/A angleVector -= self.TWO_PI
6194N/A elif angleVector < - self.PI:
6194N/A angleVector += self.TWO_PI
6194N/A # draw arc
6194N/A if angleVector >= 0:
6194N/A angle = angleStart + self.toolOffsetFlat
6194N/A while angle < angleStart + angleVector:
6194N/A self.storePoint('PD', self.vData[2][1] + math.cos(angle) * self.toolOffset, self.vData[2][2] + math.sin(angle) * self.toolOffset)
6194N/A angle += self.toolOffsetFlat
6194N/A else:
6194N/A angle = angleStart - self.toolOffsetFlat
6194N/A while angle > angleStart + angleVector:
6194N/A self.storePoint('PD', self.vData[2][1] + math.cos(angle) * self.toolOffset, self.vData[2][2] + math.sin(angle) * self.toolOffset)
6194N/A angle -= self.toolOffsetFlat
6194N/A self.storePoint('PD', pointFour[0], pointFour[1])
6194N/A
6194N/A def storePoint(self, command, x, y):
6194N/A x = int(round(x))
6194N/A y = int(round(y))
6194N/A # skip when no change in movement
6194N/A if self.lastPoint[0] == command and self.lastPoint[1] == x and self.lastPoint[2] == y:
6194N/A return
6194N/A if self.dryRun:
6194N/A # find edges
6194N/A if self.divergenceX == 'False' or x < self.divergenceX:
6194N/A self.divergenceX = x
6194N/A if self.divergenceY == 'False' or y < self.divergenceY:
6194N/A self.divergenceY = y
6194N/A if self.sizeX == 'False' or x > self.sizeX:
6194N/A self.sizeX = x
6194N/A if self.sizeY == 'False' or y > self.sizeY:
6194N/A self.sizeY = y
6194N/A else:
6194N/A # store point
6194N/A if not self.options.center:
6194N/A # only positive values are allowed (usually)
6194N/A if x < 0:
6194N/A x = 0
6194N/A if y < 0:
6194N/A y = 0
6194N/A # do not repeat command
6194N/A if command == 'PD' and self.lastPoint[0] == 'PD':
6194N/A self.hpgl += ',%d,%d' % (x, y)
6194N/A else:
6194N/A self.hpgl += ';%s%d,%d' % (command, x, y)
6194N/A self.lastPoint = [command, x, y]
6194N/A
6194N/A# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99