# coding=utf-8 # # Copyright (C) 2008 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # """ Base class for HGPL Encoding """ import re import math import inkex from inkex.transforms import Transform, DirectedLineSegment, Vector2d from inkex.bezier import cspsubdiv class NoPathError(ValueError): """Raise that paths not selected""" # Find the pen number in the layer number FIND_PEN = re.compile(r"\s*pen\s*(\d+)\s*", re.IGNORECASE) # Find the pen speed in the layer number FIND_SPEED = re.compile(r"\s*speed\s*(\d+)\s*", re.IGNORECASE) # Find pen force in the layer name FIND_FORCE = re.compile(r"\s*force\s*(\d+)\s*", re.IGNORECASE) class hpglEncoder(object): """HPGL Encoder, used by others""" def __init__(self, effect): """options: "resolutionX":float "resolutionY":float "pen":int "force:int "speed:int "orientation":string // "0", "90", "-90", "180" "mirrorX":bool "mirrorY":bool "center":bool "flat":float "overcut":float "toolOffset":float "precut":bool "autoAlign":bool """ self.options = effect.options self.doc = effect.svg self.docWidth = effect.svg.viewbox_width self.docHeight = effect.svg.viewbox_height self.hpgl = "" self.divergenceX = "False" self.divergenceY = "False" self.sizeX = "False" self.sizeY = "False" self.dryRun = True self.lastPoint = [0, 0, 0] self.lastPen = -1 self.lastSpeed = -1 self.lastForce = -1 self.offsetX = 0 self.offsetY = 0 # dots per inch to dots per user unit: self.scaleX = self.options.resolutionX / effect.svg.viewport_to_unit("1.0in") self.scaleY = self.options.resolutionY / effect.svg.viewport_to_unit("1.0in") scaleXY = (self.scaleX + self.scaleY) / 2 # mm to dots (plotter coordinate system): self.overcut = ( effect.svg.viewport_to_unit(str(self.options.overcut) + "mm") * scaleXY ) self.toolOffset = ( effect.svg.viewport_to_unit(str(self.options.toolOffset) + "mm") * scaleXY ) # scale flatness to resolution: self.flat = self.options.flat / ( 1016 / ((self.options.resolutionX + self.options.resolutionY) / 2) ) if self.toolOffset > 0.0: self.toolOffsetFlat = ( self.flat / self.toolOffset * 4.5 ) # scale flatness to offset else: self.toolOffsetFlat = 0.0 self.mirrorX = -1.0 if self.options.mirrorX else 1.0 self.mirrorY = 1.0 if self.options.mirrorY else -1.0 # process viewBox attribute to correct page scaling self.viewBoxTransformX = 1 self.viewBoxTransformY = 1 viewBox = effect.svg.get_viewbox() if viewBox and viewBox[2] and viewBox[3]: self.viewBoxTransformX = self.docWidth / effect.svg.viewport_to_unit( effect.svg.add_unit(viewBox[2]) ) self.viewBoxTransformY = self.docHeight / effect.svg.viewport_to_unit( effect.svg.add_unit(viewBox[3]) ) def getHpgl(self): """Return the HPGL instructions""" # dryRun to find edges transform = Transform( [ [self.mirrorX * self.scaleX * self.viewBoxTransformX, 0.0, 0.0], [0.0, self.mirrorY * self.scaleY * self.viewBoxTransformY, 0.0], ] ) transform.add_rotate(int(self.options.orientation)) self.vData = [ ["", "False", 0], ["", "False", 0], ["", "False", 0], ["", "False", 0], ] self.process_group(self.doc, transform) if ( self.divergenceX == "False" or self.divergenceY == "False" or self.sizeX == "False" or self.sizeY == "False" ): raise NoPathError("No paths found") # live run self.dryRun = False # move drawing according to various modifiers if self.options.autoAlign: if self.options.center: self.offsetX -= (self.sizeX - self.divergenceX) / 2 self.offsetY -= (self.sizeY - self.divergenceY) / 2 else: self.divergenceX = 0.0 self.divergenceY = 0.0 if self.options.center: if self.options.orientation == "0": self.offsetX -= (self.docWidth * self.scaleX) / 2 self.offsetY += (self.docHeight * self.scaleY) / 2 if self.options.orientation == "90": self.offsetY += (self.docWidth * self.scaleX) / 2 self.offsetX += (self.docHeight * self.scaleY) / 2 if self.options.orientation == "180": self.offsetX += (self.docWidth * self.scaleX) / 2 self.offsetY -= (self.docHeight * self.scaleY) / 2 if self.options.orientation == "270": self.offsetY -= (self.docWidth * self.scaleX) / 2 self.offsetX -= (self.docHeight * self.scaleY) / 2 else: if self.options.orientation == "0": self.offsetY += self.docHeight * self.scaleY if self.options.orientation == "90": self.offsetY += self.docWidth * self.scaleX self.offsetX += self.docHeight * self.scaleY if self.options.orientation == "180": self.offsetX += self.docWidth * self.scaleX if not self.options.center and self.toolOffset > 0.0: self.offsetX += self.toolOffset self.offsetY += self.toolOffset # initialize transformation matrix and cache transform = Transform( [ [ self.mirrorX * self.scaleX * self.viewBoxTransformX, 0.0, -float(self.divergenceX) + self.offsetX, ], [ 0.0, self.mirrorY * self.scaleY * self.viewBoxTransformY, -float(self.divergenceY) + self.offsetY, ], ] ) transform.add_rotate(int(self.options.orientation)) self.vData = [ ["", "False", 0], ["", "False", 0], ["", "False", 0], ["", "False", 0], ] # add move to zero point and precut if self.toolOffset > 0.0 and self.options.precut: if self.options.center: # position precut outside of drawing plus one time the tooloffset if self.offsetX >= 0.0: precutX = self.offsetX + self.toolOffset else: precutX = self.offsetX - self.toolOffset if self.offsetY >= 0.0: precutY = self.offsetY + self.toolOffset else: precutY = self.offsetY - self.toolOffset self.processOffset( "PU", Vector2d(precutX, precutY), self.options.pen, self.options.speed, self.options.force, ) self.processOffset( "PD", Vector2d(precutX, precutY + self.toolOffset * 8), self.options.pen, self.options.speed, self.options.force, ) else: self.processOffset( "PU", Vector2d(0, 0), self.options.pen, self.options.speed, self.options.force, ) self.processOffset( "PD", Vector2d(0, self.toolOffset * 8), self.options.pen, self.options.speed, self.options.force, ) # start conversion self.process_group(self.doc, transform) # shift an empty node in in order to process last node in cache if self.toolOffset > 0.0 and not self.dryRun: self.processOffset("PU", Vector2d(0, 0), 0, 0, 0) return self.hpgl def process_group(self, group, transform): """flatten layers and groups to avoid recursion""" for child in group: if not isinstance(child, inkex.ShapeElement): continue if child.is_visible(): if isinstance(child, inkex.Group): self.process_group(child, transform) elif isinstance(child, inkex.PathElement): self.process_path(child, transform) else: # This only works for shape elements (not text yet!) new_elem = child.replace_with(child.to_path_element()) # Element is given composed transform b/c it's not added back to doc new_elem.transform = child.composed_transform() self.process_path(new_elem, transform) def get_pen_number(self, node): """Get pen number for node label (usually group)""" for parent in [node] + list(node.ancestors()): match = FIND_PEN.search(parent.label or "") if match: return int(match.group(1)) return int(self.options.pen) def get_pen_speed(self, node): """Get pen speed for node label (usually group)""" for parent in [node] + list(node.ancestors()): match = FIND_SPEED.search(parent.label or "") if match: return int(match.group(1)) return int(self.options.speed) def get_pen_force(self, node): """Get pen force for node label (usually group)""" for parent in [node] + list(node.ancestors()): match = FIND_FORCE.search(parent.label or "") if match: return int(match.group(1)) return int(self.options.force) def process_path(self, node, transform): """Process the given element into a plotter path""" pen = self.get_pen_number(node) speed = self.get_pen_speed(node) force = self.get_pen_force(node) path = ( node.path.to_absolute() .transform(node.composed_transform()) .transform(transform) .to_superpath() ) if path: cspsubdiv(path, self.flat) # path to HPGL commands oldPosX = 0.0 oldPosY = 0.0 for singlePath in path: cmd = "PU" for singlePathPoint in singlePath: posX, posY = singlePathPoint[1] # check if point is repeating, if so, ignore if int(round(posX)) != int(round(oldPosX)) or int( round(posY) ) != int(round(oldPosY)): self.processOffset(cmd, Vector2d(posX, posY), pen, speed, force) cmd = "PD" oldPosX = posX oldPosY = posY # perform overcut if self.overcut > 0.0 and not self.dryRun: # check if last and first points are the same, otherwise the path # is not closed and no overcut can be performed if int(round(oldPosX)) == int(round(singlePath[0][1][0])) and int( round(oldPosY) ) == int(round(singlePath[0][1][1])): overcutLength = 0 for singlePathPoint in singlePath: posX, posY = singlePathPoint[1] # check if point is repeating, if so, ignore if int(round(posX)) != int(round(oldPosX)) or int( round(posY) ) != int(round(oldPosY)): overcutLength += ( Vector2d(posX, posY) - (oldPosX, oldPosY) ).length if overcutLength >= self.overcut: newEndPoint = self.changeLength( Vector2d(oldPosX, oldPosY), Vector2d(posX, posY), -(overcutLength - self.overcut), ) self.processOffset( cmd, newEndPoint, pen, speed, force ) break self.processOffset( cmd, Vector2d(posX, posY), pen, speed, force ) oldPosX = posX oldPosY = posY def changeLength(self, p1, p2, offset): """change length of line""" if p1.x == p2.x and p1.y == p2.y: # abort if points are the same return p1 return Vector2d(DirectedLineSegment(p2, p1).point_at_length(-offset)) def processOffset(self, cmd, point, pen, speed, force): """Calculate offset correction""" if self.toolOffset == 0.0 or self.dryRun: self.storePoint(cmd, point, pen, speed, force) else: # insert data into cache self.vData.pop(0) self.vData.insert(3, [cmd, point, pen, speed, force]) # decide if enough data is available if self.vData[2][1] != "False": if self.vData[1][1] == "False": self.storePoint( self.vData[2][0], self.vData[2][1], self.vData[2][2], self.vData[2][3], self.vData[2][4], ) else: # perform tool offset correction (It's a *tad* complicated, if you want # to understand it draw the data as lines on paper) 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 pointThree = self.changeLength( self.vData[1][1], self.vData[2][1], self.toolOffset ) self.storePoint( "PD", pointThree, self.vData[2][2], self.vData[2][3], self.vData[2][4], ) elif self.vData[0][1] != "False": # Elif the 1st entry in the cache is filled with data and the 3rd entry # is a pen up command shift the 3rd entry by the current tool offset # position according to the 2nd command pointThree = self.changeLength( self.vData[0][1], self.vData[1][1], self.toolOffset ) pointThree = self.vData[2][1] - (self.vData[1][1] - pointThree) self.storePoint( "PU", pointThree, self.vData[2][2], self.vData[2][3], self.vData[2][4], ) else: # Else just write the 3rd entry pointThree = self.vData[2][1] self.storePoint( "PU", pointThree, self.vData[2][2], self.vData[2][3], self.vData[2][4], ) if self.vData[3][0] == "PD": # 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 originalSegment = DirectedLineSegment( self.vData[2][1], self.vData[3][1] ) if originalSegment.length >= self.toolOffset: pointFour = self.changeLength( originalSegment.end, originalSegment.start, -self.toolOffset, ) else: pointFour = self.changeLength( originalSegment.start, originalSegment.end, self.toolOffset - originalSegment.length, ) # get angle start and angle vector angleStart = DirectedLineSegment( self.vData[2][1], pointThree ).angle angleVector = ( DirectedLineSegment(self.vData[2][1], pointFour).angle - angleStart ) # switch direction when arc is bigger than 180° if angleVector > math.pi: angleVector -= math.pi * 2 elif angleVector < -math.pi: angleVector += math.pi * 2 # draw arc if angleVector >= 0: angle = angleStart + self.toolOffsetFlat while angle < angleStart + angleVector: self.storePoint( "PD", self.vData[2][1] + self.toolOffset * Vector2d(math.cos(angle), math.sin(angle)), self.vData[2][2], self.vData[2][3], self.vData[2][4], ) angle += self.toolOffsetFlat else: angle = angleStart - self.toolOffsetFlat while angle > angleStart + angleVector: self.storePoint( "PD", self.vData[2][1] + self.toolOffset * Vector2d(math.cos(angle), math.sin(angle)), self.vData[2][2], self.vData[2][3], self.vData[2][4], ) angle -= self.toolOffsetFlat self.storePoint( "PD", pointFour, self.vData[3][2], self.vData[2][3], self.vData[2][4], ) def storePoint(self, command, point, pen, speed, force): x = int(round(point.x)) y = int(round(point.y)) # skip when no change in movement if ( self.lastPoint[0] == command and self.lastPoint[1] == x and self.lastPoint[2] == y ): return if self.dryRun: # find edges if self.divergenceX == "False" or x < self.divergenceX: self.divergenceX = x if self.divergenceY == "False" or y < self.divergenceY: self.divergenceY = y if self.sizeX == "False" or x > self.sizeX: self.sizeX = x if self.sizeY == "False" or y > self.sizeY: self.sizeY = y else: # store point if not self.options.center: # only positive values are allowed (usually) if x < 0: x = 0 if y < 0: y = 0 # select correct pen if self.lastPen != pen: self.hpgl += ";PU;SP%d" % pen if self.lastSpeed != speed: if speed > 0: self.hpgl += ";VS%d" % speed if self.lastForce != force: if force > 0: self.hpgl += ";FS%d" % force # do not repeat command if command == "PD" and self.lastPoint[0] == "PD" and self.lastPen == pen: self.hpgl += ",%d,%d" % (x, y) else: self.hpgl += ";%s%d,%d" % (command, x, y) self.lastPen = pen self.lastSpeed = speed self.lastForce = force self.lastPoint = [command, x, y]