diff options
Diffstat (limited to 'share/extensions/hpgl_encoder.py')
-rw-r--r-- | share/extensions/hpgl_encoder.py | 526 |
1 files changed, 526 insertions, 0 deletions
diff --git a/share/extensions/hpgl_encoder.py b/share/extensions/hpgl_encoder.py new file mode 100644 index 0000000..f6a30f3 --- /dev/null +++ b/share/extensions/hpgl_encoder.py @@ -0,0 +1,526 @@ +# 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] |