216 lines
8.1 KiB
Python
216 lines
8.1 KiB
Python
#!/usr/bin/env python
|
|
# Copyright 2015 Jo Pol
|
|
# 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 3 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, see http://www.gnu.org/licenses/.
|
|
|
|
from __future__ import division
|
|
from math import pi, sin, cos, tan, radians
|
|
|
|
|
|
# We will use the inkex module with the predefined
|
|
# Effect base class.
|
|
import inkex, simplestyle
|
|
|
|
__author__ = 'Jo Pol'
|
|
__credits__ = ['Veronika Irvine','Jo Pol','Mark Shafer']
|
|
__license__ = 'GPLv3'
|
|
|
|
class PolarGrid(inkex.Effect):
|
|
"""
|
|
Creates a dotted polar grid where distance between the circles
|
|
increase with the distance between the dots on the circles
|
|
"""
|
|
def __init__(self):
|
|
"""
|
|
Constructor.
|
|
"""
|
|
|
|
# Call the base class constructor.
|
|
inkex.Effect.__init__(self)
|
|
|
|
# parse the options
|
|
self.OptionParser.add_option('-a', '--angle', action='store', type='float', dest='angleOnFootside', default=45, help='grid angle (degrees)')
|
|
self.OptionParser.add_option('-d', '--dots', action='store', type='int', dest='dotsPerCircle', default=180, help='number of dots on a circle')
|
|
self.OptionParser.add_option('-o', '--outerDiameter', action='store', type='float', dest='outerDiameter', default=160, help='outer diameter (mm)')
|
|
self.OptionParser.add_option('-i', '--innerDiameter', action='store', type='float', dest='innerDiameter', default=100, help='minimum inner diameter (mm)')
|
|
self.OptionParser.add_option('-f', '--fill', action='store', type='string', dest='dotFill', default='-6711040', help='dot color')
|
|
self.OptionParser.add_option('-A', '--alignment', action='store', type='string', dest='alignment', default='outside', help='exact diameter on [inside|outside]')
|
|
self.OptionParser.add_option('-s', '--size', action='store', type='float', dest='dotSize', default=0.5, help='dot diameter (mm)')
|
|
self.OptionParser.add_option('-v', '--variant', action='store', type='string', dest='variant', default='', help='omit rows to get [|rectangle|hexagon1]')
|
|
self.OptionParser.add_option('-u', '--units', action = 'store', type = 'string', dest = 'units', default = 'mm', help = 'The units the measurements are in')
|
|
|
|
def group(self, diameter):
|
|
"""
|
|
Create a group labeled with the diameter
|
|
"""
|
|
label = 'diameter: {0:.2f} mm'.format(diameter)
|
|
attribs = {inkex.addNS('label', 'inkscape'):label}
|
|
return inkex.etree.SubElement(self.gridContainer, inkex.addNS('g', 'svg'), attribs)
|
|
|
|
def dots(self, diameter, circleNr, group):
|
|
"""
|
|
Draw dots on a grid circle
|
|
"""
|
|
offset = (circleNr % 2) * 0.5
|
|
for dotNr in range (0, self.options.dotsPerCircle):
|
|
a = (dotNr + offset) * self.alpha
|
|
x = (diameter / 2.0) * cos(a)
|
|
y = (diameter / 2.0) * sin(a)
|
|
attribs = {'style':self.dotStyle, 'cx':str(x * self.scale), 'cy':str(y * self.scale), 'r':self.dotR}
|
|
inkex.etree.SubElement(group, inkex.addNS('circle', 'svg'), attribs)
|
|
|
|
def getUnittouu(self, param):
|
|
" compatibility between inkscape 0.48 and 0.91 "
|
|
try:
|
|
return inkex.unittouu(param)
|
|
except AttributeError:
|
|
return self.unittouu(param)
|
|
|
|
def getColorString(self, longColor, verbose=False):
|
|
""" Convert the long into a #RRGGBB color value
|
|
- verbose=true pops up value for us in defaults
|
|
conversion back is A + B*256^1 + G*256^2 + R*256^3
|
|
"""
|
|
if verbose: inkex.debug("%s ="%(longColor))
|
|
longColor = long(longColor)
|
|
if longColor <0: longColor = long(longColor) & 0xFFFFFFFF
|
|
hexColor = hex(longColor)[2:-3]
|
|
hexColor = '#' + hexColor.rjust(6, '0').upper()
|
|
if verbose: inkex.debug(" %s for color default value"%(hexColor))
|
|
return hexColor
|
|
|
|
def iterate(self, diameter, circleNr):
|
|
"""
|
|
Create a group with a ring of dots.
|
|
Returns half of the arc length between the dots
|
|
which becomes the distance to the next ring.
|
|
"""
|
|
group = self.group(diameter)
|
|
self.dots(diameter, circleNr, group)
|
|
self.generatedCircles.append(group)
|
|
return diameter * self.change
|
|
|
|
def generate(self):
|
|
"""
|
|
Generate rings with dots, either inside out or outside in
|
|
"""
|
|
circleNr = 0
|
|
flag_error = False
|
|
minimum = 2 * self.options.dotSize * self.options.dotsPerCircle /pi
|
|
if minimum < self.options.innerDiameter:
|
|
minimum = self.options.innerDiameter
|
|
else:
|
|
flag_error = True
|
|
if self.options.alignment == 'outside':
|
|
diameter = self.options.outerDiameter
|
|
while diameter > minimum:
|
|
diameter -= self.iterate(diameter, circleNr)
|
|
circleNr += 1
|
|
else:
|
|
diameter = minimum
|
|
while diameter < self.options.outerDiameter:
|
|
diameter += self.iterate(diameter, circleNr)
|
|
circleNr += 1
|
|
# Display message
|
|
if flag_error:
|
|
# Leave message on top
|
|
font_height = 8
|
|
text_style = { 'font-size': str(font_height),
|
|
'font-family': 'sans-serif',
|
|
'text-anchor': 'middle',
|
|
'text-align': 'center',
|
|
'fill': '#000000' }
|
|
text_atts = {'style':simplestyle.formatStyle(text_style),
|
|
'x': '0', 'y': '0'}
|
|
text = inkex.etree.SubElement(self.gridContainer, 'text', text_atts)
|
|
text.text = "Dots overlap. inner changed to %4.1f" % (minimum)
|
|
|
|
def removeGroups(self, start, increment):
|
|
"""
|
|
Remove complete rings with dots
|
|
"""
|
|
for i in range(start, len(self.generatedCircles), increment):
|
|
self.current_layer.remove(self.generatedCircles[i])
|
|
|
|
def removeDots(self, i, offset, step):
|
|
"""
|
|
Remove dots from one circle
|
|
"""
|
|
group = self.generatedCircles[i]
|
|
dots = list(group)
|
|
start = len(dots) - 1 - offset
|
|
for j in range(start, -1, 0-step):
|
|
group.remove(dots[j])
|
|
|
|
def computations(self, angle):
|
|
self.alpha = radians(360.0 / self.options.dotsPerCircle)
|
|
correction = pi / (4 * self.options.dotsPerCircle)
|
|
correction *= tan(angle*0.93)
|
|
self.change = tan(angle - correction) * pi / self.options.dotsPerCircle
|
|
|
|
def effect(self):
|
|
"""
|
|
Effect behaviour.
|
|
Overrides base class' method and draws something.
|
|
"""
|
|
|
|
# color
|
|
self.options.dotFill = self.getColorString(self.options.dotFill)
|
|
# constants
|
|
self.dotStyle = simplestyle.formatStyle({'fill': self.options.dotFill,'stroke':'none'})
|
|
self.scale = self.getUnittouu("1" + self.options.units)
|
|
self.dotR = str(self.options.dotSize * (self.scale/2))
|
|
self.computations(radians(self.options.angleOnFootside))
|
|
|
|
# processing variables
|
|
self.generatedCircles = []
|
|
self.gridContainer = self.current_layer
|
|
|
|
self.generate()
|
|
|
|
if self.options.variant == 'rectangle':
|
|
self.removeGroups(1, 2)
|
|
elif self.options.variant == 'hexagon1':
|
|
self.removeGroups(0, 3)
|
|
elif self.options.variant == 'hexagon2' or self.options.variant == 'snow2':
|
|
for i in range(0, len(self.generatedCircles), 1):
|
|
self.removeDots(i, (((i%2)+1)*2)%3, 3)
|
|
elif self.options.variant == 'hexagon3':
|
|
for i in range(0, len(self.generatedCircles), 2):
|
|
self.removeDots(i, (i//2+1)%2, 2)
|
|
elif self.options.variant == 'hexagon4':
|
|
self.removeGroups(0, 4)
|
|
elif self.options.variant == 'hexagon5' or self.options.variant == 'snow1':
|
|
for i in range(0, len(self.generatedCircles), 2):
|
|
self.removeDots(i, 1, 2)
|
|
|
|
self.dotStyle = simplestyle.formatStyle({'fill': 'none','stroke':self.options.dotFill,'stroke-width':0.7})
|
|
self.dotR = str((((self.options.innerDiameter * pi) / self.options.dotsPerCircle) / 2) * self.scale)
|
|
self.generatedCircles = []
|
|
if self.options.variant == 'snow2':
|
|
self.options.dotsPerCircle = self.options.dotsPerCircle // 3
|
|
self.computations(radians(self.options.angleOnFootside))
|
|
self.generate()
|
|
elif self.options.variant == 'snow1':
|
|
self.generate()
|
|
self.removeGroups(1, 2)
|
|
for i in range(0, len(self.generatedCircles), 2):
|
|
self.removeDots(i, i%4, 2)
|
|
for i in range(0, len(self.generatedCircles), 2):
|
|
self.removeDots(i, (i+1)%2, 2)
|
|
for i in range(2, len(self.generatedCircles), 4):
|
|
self.removeDots(i, 0, self.options.dotsPerCircle)
|
|
|
|
# Create effect instance and apply it.
|
|
if __name__ == '__main__':
|
|
PolarGrid().affect() |