532 lines
22 KiB
Python
532 lines
22 KiB
Python
# 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:
|
|
non_empty_subpath = False
|
|
for i, singlePathPoint in enumerate(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)):
|
|
non_empty_subpath = True
|
|
self.processOffset(
|
|
"PU" if not i else "PD",
|
|
Vector2d(posX, posY),
|
|
pen,
|
|
speed,
|
|
force,
|
|
)
|
|
oldPosX = posX
|
|
oldPosY = posY
|
|
# perform overcut
|
|
if self.overcut > 0.0 and not self.dryRun and non_empty_subpath:
|
|
# 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(
|
|
"PD", newEndPoint, pen, speed, force
|
|
)
|
|
break
|
|
self.processOffset(
|
|
"PD", 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]
|