summaryrefslogtreecommitdiffstats
path: root/share/extensions/hpgl_encoder.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
commitcca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch)
tree146f39ded1c938019e1ed42d30923c2ac9e86789 /share/extensions/hpgl_encoder.py
parentInitial commit. (diff)
downloadinkscape-cca66b9ec4e494c1d919bff0f71a820d8afab1fa.tar.xz
inkscape-cca66b9ec4e494c1d919bff0f71a820d8afab1fa.zip
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'share/extensions/hpgl_encoder.py')
-rw-r--r--share/extensions/hpgl_encoder.py526
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]