470 lines
16 KiB
Python
470 lines
16 KiB
Python
#!/usr/bin/env python2
|
|
|
|
|
|
# Copyright 2016 Luke Phillips (lukerazor@hotmail.com)
|
|
# 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 St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
|
|
# Extension dirs
|
|
# linux:~/.config/inkscape/extensions
|
|
# windows: [Drive]:\Program Files\Inkscape\share\extensions
|
|
|
|
import inkex
|
|
import simplestyle
|
|
import copy
|
|
import math
|
|
|
|
FOLD_GAP = 5
|
|
CROP_GAP = 2
|
|
CROP_LENGTH = 3
|
|
|
|
CSNS = ""
|
|
|
|
inkex.NSS[u'cs'] = u'http://www.razorfoss.org/tuckboxextension/'
|
|
|
|
def PrintDebug(string):
|
|
inkex.debug( _(str(string)) )
|
|
|
|
def RoundAndDeduplicatePoints(points):
|
|
return sorted(list(set(map(lambda x: round(x, 3), points))))
|
|
|
|
class Point():
|
|
def __init__(self, x, y):
|
|
self.x = x
|
|
self.y = y
|
|
|
|
def rotate(self, angle, origin):
|
|
"""
|
|
Rotate a point counterclockwise by a given angle around a given origin.
|
|
|
|
The angle should be given in degrees.
|
|
"""
|
|
rads = math.radians(angle)
|
|
newX = origin.x + math.cos(rads) * (self.x - origin.x) - math.sin(rads) * (self.y - origin.y)
|
|
newY = origin.y + math.sin(rads) * (self.x - origin.x) + math.cos(rads) * (self.y - origin.y)
|
|
|
|
return Point(newX, newY)
|
|
|
|
def add(self, point):
|
|
return Point(self.x + point.x, self.y + point.y)
|
|
|
|
@staticmethod
|
|
def parsePoint(pointString):
|
|
x, y = map(lambda v: float(v), pointString.split(","))
|
|
return Point(x, y)
|
|
|
|
@staticmethod
|
|
def parse(pointString, orientationString=None):
|
|
p1 = Point.parsePoint(pointString)
|
|
p = Point(p1.x, p1.y)
|
|
|
|
if orientationString != None:
|
|
po = Point.parsePoint(orientationString)
|
|
p = p1.add(po.rotate(270, Point(0, 0)))
|
|
|
|
return p
|
|
|
|
class HexGeneratorBase(object):
|
|
def __init__(self, hexDiameter, hexMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc):
|
|
self.UnitConverterFunc = unitConverterFunc
|
|
self.HexDiameter = hexDiameter
|
|
self.HexMargin = hexMargin
|
|
self.BleedMargin = bleedMargin
|
|
|
|
self.PageWidth = pageWidth
|
|
self.PageHeight = pageHeight
|
|
self.PageMargin = pageMargin
|
|
|
|
self.ContainerWidth = -1
|
|
self.ContainerHeight = -1
|
|
|
|
def CalcPageLeftMargin(self):
|
|
return (self.PageWidth - self.ContentWidth) / 2.0
|
|
|
|
def CalcPageBottomMargin(self):
|
|
return (self.PageHeight - self.ContentHeight) / 2.0
|
|
|
|
def DrawGuide(self, xmlParent, xpos, ypos):
|
|
posString = "{},{}".format(xpos, ypos)
|
|
attribs = {'position': posString, 'orientation': posString}
|
|
|
|
inkex.etree.SubElement(xmlParent, inkex.addNS('guide',"sodipodi"), attribs)
|
|
|
|
def DrawAngledGuide(self, xmlParent, xpos, ypos, angle):
|
|
# Angles are taken from the horizontal axis, positive angles move clockwise
|
|
posString = "{},{}".format(xpos, ypos)
|
|
orientationString = "{}, {}".format(math.sin(math.radians(angle)), math.cos(math.radians(angle)))
|
|
attribs = {'position': posString, 'orientation': orientationString}
|
|
|
|
inkex.etree.SubElement(xmlParent, inkex.addNS('guide',"sodipodi"), attribs)
|
|
|
|
def ConvertPoint(self, p):
|
|
# convert point into svg approriate values, including catering for inkscapes "alternative" axis sytem ie 0, 0 is bottom left not top left
|
|
newX = self.UnitConverterFunc("{}mm".format(p.x))
|
|
newY = self.PageHeight - self.UnitConverterFunc("{}mm".format(p.y))
|
|
|
|
return Point(newX, newY)
|
|
|
|
def DrawLine(self, xmlParent, p1, p2):
|
|
cp1 = self.ConvertPoint(p1)
|
|
cp2 = self.ConvertPoint(p2)
|
|
|
|
pathStr = "M {},{} {}, {}".format(cp1.x, cp1.y, cp2.x, cp2.y)
|
|
style = {'stroke': '#000000', 'stroke-width': self.UnitConverterFunc('0.25mm'), 'fill': 'none'}
|
|
attribs = {'style': simplestyle.formatStyle(style), 'd': pathStr}
|
|
|
|
inkex.etree.SubElement(xmlParent, inkex.addNS('path','svg'), attribs )
|
|
|
|
def DrawVerticleGuides(self, xmlParent, positions, gap):
|
|
curPos = self.CalcPageLeftMargin()
|
|
lastPos = -1
|
|
while curPos + self.ContainerWidth <= self.PageWidth - self.PageMargin:
|
|
for offset in positions:
|
|
curPos += offset
|
|
if curPos != lastPos: # don't double draw
|
|
self.DrawGuide(xmlParent, curPos, 0)
|
|
|
|
lastPos = curPos
|
|
|
|
curPos += gap
|
|
|
|
def DrawHorizontalGuides(self, xmlParent, positions, gap):
|
|
curPos = self.CalcPageBottomMargin()
|
|
lastPos = -1
|
|
while curPos + self.ContainerHeight <= self.PageHeight - self.PageMargin:
|
|
for offset in positions:
|
|
curPos += offset
|
|
if curPos != lastPos: # don't double draw
|
|
self.DrawGuide(xmlParent, 0, curPos)
|
|
|
|
lastPos = curPos
|
|
|
|
curPos += gap
|
|
|
|
def DrawAngledGuides(self, xmlParent, offsetPositions, angle, gap):
|
|
|
|
numExtraTopContainers = 0
|
|
numExtraBottomContainers = 0
|
|
if angle > 0:
|
|
numExtraTopContainers = math.ceil(self.NumContainersAcross / 2.0) - 1
|
|
if angle < 0:
|
|
numExtraBottomContainers = math.ceil(self.NumContainersAcross / 2.0) - 1
|
|
|
|
# draw sets of guides per point avoiding duplicate lines
|
|
# NOTE: Effectivly we draw the bottom guides first and then move up (ie y is increasing)
|
|
curPos = self.CalcPageBottomMargin() - numExtraBottomContainers * self.ContainerHeight
|
|
lastPos = -1
|
|
|
|
while curPos + self.ContainerHeight <= self.PageHeight - self.PageMargin + numExtraTopContainers*self.ContainerHeight:
|
|
for offset in offsetPositions:
|
|
curPos += offset
|
|
if curPos != lastPos: # don't double draw
|
|
self.DrawAngledGuide(xmlParent, self.CalcPageLeftMargin() + self.ContainerWidth/2, curPos, angle)
|
|
|
|
lastPos = curPos
|
|
|
|
curPos += gap
|
|
|
|
def GenerateFoldLines(self, xmlParent):
|
|
lines = self.GetFoldLinePositions()
|
|
|
|
for line in lines:
|
|
self.DrawLine(xmlParent, line[0], line[1])
|
|
|
|
def GenerateCropMarks(self, xmlParent):
|
|
lines = self.GetCropMarkLines()
|
|
|
|
for line in lines:
|
|
self.DrawLine(xmlParent, line[0], line[1])
|
|
|
|
@staticmethod
|
|
def CalculateBorderVerticalOffset(borderWidth):
|
|
if borderWidth == 0:
|
|
return 0
|
|
|
|
return borderWidth / math.sin(math.radians(60))
|
|
|
|
@staticmethod
|
|
def CreateHexGenerator(hexDiameter, hexMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc):
|
|
return SimpleHexGridLineGenerator(hexDiameter, hexMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc)
|
|
|
|
class SimpleHexGridLineGenerator(HexGeneratorBase):
|
|
def __init__(self, hexDiameter, hexMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc):
|
|
super(SimpleHexGridLineGenerator, self).__init__(hexDiameter, hexMargin, bleedMargin, pageWidth, pageHeight, pageMargin, unitConverterFunc)
|
|
|
|
self.HexWidth = math.sqrt(3) * (hexDiameter/2)
|
|
self.ContainerWidth = self.HexWidth + 2*bleedMargin
|
|
self.ContainerHeight = hexDiameter + 2*HexGeneratorBase.CalculateBorderVerticalOffset(bleedMargin)
|
|
|
|
# num across
|
|
# num down
|
|
self.NumContainersAcross = int((self.PageWidth - 2*self.PageMargin) // self.ContainerWidth) # round down division
|
|
self.NumContainersDown = int((self.PageHeight - 2*self.PageMargin) // self.ContainerHeight) # round down division
|
|
|
|
# content sizes
|
|
self.ContentWidth = self.NumContainersAcross * self.ContainerWidth
|
|
self.ContentHeight = self.NumContainersDown * self.ContainerHeight
|
|
|
|
def GenerateGuides(self, xmlParent):
|
|
verticalGuideOffsets = [
|
|
0,
|
|
self.BleedMargin,
|
|
self.HexMargin,
|
|
self.HexWidth - 2*self.HexMargin,
|
|
self.HexMargin,
|
|
self.BleedMargin
|
|
]
|
|
|
|
bleedVerticalOffset = HexGeneratorBase.CalculateBorderVerticalOffset(self.BleedMargin)
|
|
hexMarginVerticalOffset = HexGeneratorBase.CalculateBorderVerticalOffset(self.HexMargin)
|
|
horizontalGuideOffsets = [
|
|
0,
|
|
bleedVerticalOffset,
|
|
hexMarginVerticalOffset,
|
|
self.HexDiameter - 2*hexMarginVerticalOffset,
|
|
hexMarginVerticalOffset,
|
|
bleedVerticalOffset
|
|
]
|
|
|
|
self.DrawVerticleGuides(xmlParent, verticalGuideOffsets, 0)
|
|
self.DrawAngledGuides(xmlParent, horizontalGuideOffsets, -30, 0)
|
|
self.DrawAngledGuides(xmlParent, horizontalGuideOffsets, 30, 0)
|
|
|
|
def GetFoldLinePositions(self):
|
|
return [] # no fold lines in simple grid
|
|
|
|
def GetCropMarkLines(self):
|
|
lines = []
|
|
|
|
leftMargin = self.CalcPageLeftMargin()
|
|
bottomMargin = self.CalcPageBottomMargin()
|
|
bleedVerticalOffset = HexGeneratorBase.CalculateBorderVerticalOffset(self.BleedMargin)
|
|
|
|
def CalcOppositeDeltaGivenAdjacentDelta(xdelta, angle):
|
|
return math.tan(math.radians(angle)) * xdelta
|
|
|
|
#---------------------------------------------------------------------------------
|
|
#determine all vertical crop marks, duplicates possible
|
|
# figure out the xpos'
|
|
vertical_xpos = []
|
|
for idx in range(self.NumContainersAcross):
|
|
leftX = self.BleedMargin
|
|
rightX = leftX + self.HexWidth
|
|
containerOffset = leftMargin + idx * self.ContainerWidth
|
|
|
|
vertical_xpos.append(containerOffset + leftX)
|
|
vertical_xpos.append(containerOffset + rightX)
|
|
|
|
vertical_xpos = RoundAndDeduplicatePoints(vertical_xpos)
|
|
|
|
# add to list of lines
|
|
vertical_ypos = [bottomMargin - CROP_GAP, self.PageHeight - bottomMargin + CROP_GAP + CROP_LENGTH]
|
|
for xpos in vertical_xpos:
|
|
for ypos in vertical_ypos:
|
|
lines.append([
|
|
Point(xpos, ypos),
|
|
Point(xpos, ypos - CROP_LENGTH)
|
|
])
|
|
|
|
#---------------------------------------------------------------------------------
|
|
# figure out NW, SW, NE and SE crop marks for both sides of the page
|
|
vertical_ypos_nw = []
|
|
vertical_ypos_sw = []
|
|
vertical_ypos_ne = []
|
|
vertical_ypos_se = []
|
|
|
|
yoffset = CalcOppositeDeltaGivenAdjacentDelta(self.ContainerWidth/2 + CROP_GAP, 30)
|
|
|
|
lastColumnHasHalfStep = (self.NumContainersAcross % 2) == 0 # an even numbered containers across means that the last column is 1/2 a continer up and has 1 less container
|
|
staggeredContainerOffset = 0
|
|
if lastColumnHasHalfStep:
|
|
staggeredContainerOffset = self.ContainerHeight/2
|
|
|
|
for idx in range(self.NumContainersDown):
|
|
leftContainerOffset = idx * self.ContainerHeight + bottomMargin
|
|
rightContainerOffset = leftContainerOffset + staggeredContainerOffset
|
|
|
|
bottomY = bleedVerticalOffset
|
|
topY = self.ContainerHeight - bleedVerticalOffset
|
|
|
|
vertical_ypos_nw.append(leftContainerOffset + topY + yoffset)
|
|
vertical_ypos_nw.append(leftContainerOffset + bottomY + yoffset)
|
|
|
|
vertical_ypos_sw.append(leftContainerOffset + topY - yoffset)
|
|
vertical_ypos_sw.append(leftContainerOffset + bottomY - yoffset)
|
|
|
|
vertical_ypos_ne.append(rightContainerOffset + topY + yoffset)
|
|
vertical_ypos_ne.append(rightContainerOffset + bottomY + yoffset)
|
|
|
|
vertical_ypos_se.append(rightContainerOffset + topY - yoffset)
|
|
vertical_ypos_se.append(rightContainerOffset + bottomY - yoffset)
|
|
|
|
# sort out a staggered last col
|
|
if lastColumnHasHalfStep: # if it's a half step column we need to remove the last container and add acouple of extra lines
|
|
vertical_ypos_ne = vertical_ypos_ne[:-2]
|
|
vertical_ypos_se = vertical_ypos_se[:-2]
|
|
vertical_ypos_ne.append(max(vertical_ypos_ne) + 2*bleedVerticalOffset)
|
|
vertical_ypos_se.append(min(vertical_ypos_se) - 2*bleedVerticalOffset)
|
|
|
|
# remove duplicate positions
|
|
vertical_ypos_nw = RoundAndDeduplicatePoints(vertical_ypos_nw)
|
|
vertical_ypos_sw = RoundAndDeduplicatePoints(vertical_ypos_sw)
|
|
vertical_ypos_ne = RoundAndDeduplicatePoints(vertical_ypos_ne)
|
|
vertical_ypos_se = RoundAndDeduplicatePoints(vertical_ypos_se)
|
|
|
|
# add to list of lines
|
|
xpos_left = leftMargin - CROP_GAP
|
|
xpos_right = self.PageWidth - leftMargin + CROP_GAP
|
|
yoffset = CalcOppositeDeltaGivenAdjacentDelta(CROP_LENGTH, 30)
|
|
|
|
for ypos in vertical_ypos_nw:
|
|
lines.append([
|
|
Point(xpos_left, ypos),
|
|
Point(xpos_left - CROP_LENGTH, ypos + yoffset)
|
|
])
|
|
|
|
for ypos in vertical_ypos_sw:
|
|
lines.append([
|
|
Point(xpos_left, ypos),
|
|
Point(xpos_left - CROP_LENGTH, ypos - yoffset)
|
|
])
|
|
|
|
for ypos in vertical_ypos_ne:
|
|
lines.append([
|
|
Point(xpos_right, ypos),
|
|
Point(xpos_right + CROP_LENGTH, ypos + yoffset)
|
|
])
|
|
|
|
for ypos in vertical_ypos_se:
|
|
lines.append([
|
|
Point(xpos_right, ypos),
|
|
Point(xpos_right + CROP_LENGTH, ypos - yoffset)
|
|
])
|
|
|
|
#---------------------------------------------------------------------------------
|
|
# figure out NW, SW, NE and SE crop marks for top and bottom of the page
|
|
xpos_nw = []
|
|
xpos_sw = []
|
|
xpos_ne = []
|
|
xpos_se = []
|
|
|
|
xoffset_near = CalcOppositeDeltaGivenAdjacentDelta(bleedVerticalOffset + CROP_GAP, 60)
|
|
xoffset_far = CalcOppositeDeltaGivenAdjacentDelta(self.ContainerHeight - bleedVerticalOffset + CROP_GAP, 60)
|
|
|
|
for idx in range(0, self.NumContainersAcross, 2): #we only need to do every other container because of the stepping
|
|
containerOffset = idx * self.ContainerWidth + leftMargin
|
|
topY = self.ContainerHeight - bleedVerticalOffset
|
|
bottomY = bleedVerticalOffset
|
|
xpos = self.ContainerWidth/2
|
|
|
|
xpos_nw.append(containerOffset + xpos - xoffset_near)
|
|
xpos_nw.append(containerOffset + xpos - xoffset_far)
|
|
|
|
xpos_sw.append(containerOffset + xpos - xoffset_near)
|
|
xpos_sw.append(containerOffset + xpos - xoffset_far)
|
|
|
|
xpos_ne.append(containerOffset + xpos + xoffset_near)
|
|
xpos_ne.append(containerOffset + xpos + xoffset_far)
|
|
|
|
xpos_se.append(containerOffset + xpos + xoffset_near)
|
|
xpos_se.append(containerOffset + xpos + xoffset_far)
|
|
|
|
|
|
# remove duplicate positions
|
|
xpos_nw = RoundAndDeduplicatePoints(xpos_nw)
|
|
xpos_sw = RoundAndDeduplicatePoints(xpos_sw)
|
|
xpos_ne = RoundAndDeduplicatePoints(xpos_ne)
|
|
xpos_se = RoundAndDeduplicatePoints(xpos_se)
|
|
|
|
# add to list of lines
|
|
ypos_bottom = bottomMargin - CROP_GAP
|
|
ypos_top = self.PageHeight - bottomMargin + CROP_GAP
|
|
yoffset = CalcOppositeDeltaGivenAdjacentDelta(CROP_LENGTH, 30)
|
|
|
|
for xpos in xpos_nw:
|
|
if xpos > 0 and xpos < self.PageWidth:
|
|
lines.append([
|
|
Point(xpos, ypos_top),
|
|
Point(xpos - CROP_LENGTH, ypos_top + yoffset)
|
|
])
|
|
|
|
for xpos in xpos_sw:
|
|
if xpos > 0 and xpos < self.PageWidth:
|
|
lines.append([
|
|
Point(xpos, ypos_bottom),
|
|
Point(xpos - CROP_LENGTH, ypos_bottom - yoffset)
|
|
])
|
|
|
|
for xpos in xpos_ne:
|
|
if xpos > 0 and xpos < self.PageWidth:
|
|
lines.append([
|
|
Point(xpos, ypos_top),
|
|
Point(xpos + CROP_LENGTH, ypos_top + yoffset)
|
|
])
|
|
|
|
for xpos in xpos_se:
|
|
if xpos > 0 and xpos < self.PageWidth:
|
|
lines.append([
|
|
Point(xpos, ypos_bottom),
|
|
Point(xpos + CROP_LENGTH, ypos_bottom - yoffset)
|
|
])
|
|
|
|
#---------------------------------------------------------------------------------
|
|
return lines
|
|
|
|
class HexLayoutGuidesEffect(inkex.Effect):
|
|
def __init__(self):
|
|
inkex.Effect.__init__(self)
|
|
|
|
self.OptionParser.add_option('-d', '--hex_diameter', action = 'store', type = 'float', dest = 'HexDiameter')
|
|
self.OptionParser.add_option('-c', '--hex_margin', action = 'store', type = 'float', dest = 'HexMargin')
|
|
self.OptionParser.add_option('-b', '--bleed_margin', action = 'store', type = 'float', dest = 'BleedMargin')
|
|
self.OptionParser.add_option('-p', '--page_margin', action = 'store', type = 'float', dest = 'PageMargin')
|
|
|
|
def effect(self):
|
|
# find dimensions of page
|
|
pageWidth = self.uutounit(self.unittouu(self.getDocumentWidth()), "mm")
|
|
pageHeight = self.uutounit(self.unittouu(self.getDocumentHeight()), "mm")
|
|
|
|
opt = self.options
|
|
|
|
guideParent = self.document.xpath('//sodipodi:namedview',namespaces=inkex.NSS)[0]
|
|
|
|
### GUIDES
|
|
# remove all the existing guides
|
|
[node.getparent().remove(node) for node in self.document.xpath('//sodipodi:guide',namespaces=inkex.NSS)]
|
|
|
|
# create the object generator
|
|
gen = HexGeneratorBase.CreateHexGenerator(opt.HexDiameter, opt.HexMargin, opt.BleedMargin, pageWidth, pageHeight, opt.PageMargin, self.unittouu)
|
|
|
|
gen.GenerateGuides(guideParent)
|
|
|
|
### CROP MARKS
|
|
# remove any existing 'Crop marks' layer
|
|
[node.getparent().remove(node) for node in self.document.xpath("//svg:g[@inkscape:label='Crop Marks']",namespaces=inkex.NSS)]
|
|
|
|
svg = self.document.xpath('//svg:svg', namespaces=inkex.NSS)[0]
|
|
layer = inkex.etree.SubElement(svg, inkex.addNS('g',"svg"), {})
|
|
layer.set(inkex.addNS('label', 'inkscape'), "Crop Marks")
|
|
layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
|
|
#layer.set(inkex.addNS('insensitive', 'sodipodi'), 'true')
|
|
|
|
|
|
gen.GenerateCropMarks(layer)
|
|
|
|
if __name__ == '__main__':
|
|
effect = HexLayoutGuidesEffect()
|
|
|
|
effect.affect()
|