Added hexmap extension

This commit is contained in:
Mario Voigt 2020-08-17 18:11:51 +02:00
parent bcd430d2c8
commit a282b316b6
3 changed files with 483 additions and 1 deletions

View File

@ -5,7 +5,7 @@
<param name="introduction" type="description">Uses lines in text file to edit attributes of elements.</param>
<param name="usage" type="description">Line: 'elementID,attributeName,attributeValue'.</param>
<param name="namespaceTip" type="description">For namespaces use {namespaceUrl}attributeName</param>
<param name="data" type="path" _gui-text="Data file:" mode="file" filetypes="txt,csv"/>
<param name="data" type="path" gui-text="Data file:" mode="file" filetypes="txt,csv"/>
<effect>
<object-type>all</object-type>
<effects-menu>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Create Hexmap</name>
<id>fablabchemnitz.de.hexmap</id>
<param name="tab" type="notebook">
<page name="page_1" gui-text="Size">
<param name="units" type="optiongroup" appearance="combo" gui-text="Size units">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="pt">pt</option>
<option value="px">px</option>
</param>
<param name="cols" type="int" gui-text="Columns" min="1" max="1000000">10</param>
<param name="rows" type="int" gui-text="Rows" min="1" max="1000000">10</param>
<param name="hexsize" type="float" min="0" max="9999" gui-text="Hex Size (optional)" />
<param name="strokewidth" type="float" min="0.0" max="9999.0" gui-text="Stroke Width">1.0</param>
<param name="verticesize" type="float" min="0.0" max="50.0" gui-text="Size of vertices (%)">10.0</param>
</page>
<page name="page_2" gui-text="Style">
<param name="bricks" type="bool" gui-text="Bricks">false</param>
<param name="squarebricks" type="bool" gui-text="Force Square Bricks">false</param>
<param name="rotate" type="bool" gui-text="Rotate">false</param>
<param name="halfhexes" type="bool" gui-text="Half hexes at top and bottom">false</param>
<param name="xshift" type="bool" gui-text="Shift grid to side and wrap">false</param>
<param name="firstcoldown" type="bool" gui-text="First column half-hex down">false</param>
</page>
<page name="page_3" gui-text="Coords">
<param name="coordseparator" type="string" gui-text="Coordinate Separator">.</param>
<param name="coordalphacol" type="bool" gui-text="Column Alpha Coordinates">false</param>
<param name="coordrevrow" type="bool" gui-text="Row Coordinates Reversed">false</param>
<param name="coordrowfirst" type="bool" gui-text="Row Coordinate First">false</param>
<param name="coordzeros" type="bool" gui-text="Zero-Padded Coordinates">true</param>
<param name="coordrows" type="int" min="1" max="100" gui-text="Coordinates Every ... Rows">1</param>
<param name="coordcolstart" type="int" gui-text="First Col Nr" min="0" max="1000">1</param>
<param name="coordrowstart" type="int" gui-text="First Row Nr" min="0" max="1000">1</param>
</page>
<page name="page_4" gui-text="Layers">
<param name="layer_grid" type="bool" gui-text="Grid">true</param>
<param name="layer_fill" type="bool" gui-text="Fill (#ffffff)">true</param>
<param name="layer_coordinates" type="bool" gui-text="Coordinates">true</param>
<param name="layer_centerdots" type="bool" gui-text="Centerdots">true</param>
<param name="layer_vertices" type="bool" gui-text="Vertices">false</param>
<param name="layer_circles" type="bool" gui-text="Circles">false</param>
<param name="layersingroup" type="bool" gui-text="Layers in Group">false</param>
</page>
<page name="page_5" gui-text="Debug">
<param name="generatelog" type="bool" gui-text="Generate log file">false</param>
<param name="logfilepath" type="path" gui-text="Log File (optional)" mode="file_new" filetypes="txt">debug.txt</param>
</page>
</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Grids/Guides"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">fablabchemnitz_hexmap.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,419 @@
#!/usr/bin/env python3
import inkex
import sys
from inkex import NSS
import math
from lxml import etree
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return '%f,%f' % (self.x, self.y)
def y_mirror(self, h):
return Point(self.x, h - self.y);
def __sub__(self, other):
return Point(self.x - other.x, self.y - other.y)
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __mul__(self, k):
return Point(self.x * k, self.y * k)
def rotated(self, total_width):
return Point(self.y, total_width - self.x)
def nrdigits(f):
return int(math.floor(math.log10(f))) + 1
def alphacol(c):
d = c / 26
r = c % 26
return ('%c' % (r + 65)) * (int(d) + 1)
def calc_hex_height(hex_width):
return 0.25 * hex_width / math.tan(math.pi / 6) * 2
COORD_SIZE_PART_OF_HEX_HEIGHT = 0.1
COORD_YOFFSET_PART = 75
CENTERDOT_SIZE_FACTOR = 1.1690625
class HexmapEffect(inkex.Effect):
def __init__(self):
inkex.Effect.__init__(self)
self.arg_parser.add_argument('--tab')
self.arg_parser.add_argument('--generatelog', type = inkex.Boolean, default = False)
self.arg_parser.add_argument('--logfilepath', default = "debug.txt")
self.arg_parser.add_argument("--units", default='mm', help="Units this dialog is using")
self.arg_parser.add_argument('--cols', type = int, default = '10', help = 'Number of columns.')
self.arg_parser.add_argument('--rows', type = int, default = '10', help = 'Number of columns.')
self.arg_parser.add_argument('--hexsize', type = float, default = 0.0)
self.arg_parser.add_argument('--strokewidth', type = float, default = 1.0)
self.arg_parser.add_argument('--coordrows', type = int, default = '1')
self.arg_parser.add_argument('--coordcolstart', type = int, default = '1')
self.arg_parser.add_argument('--coordrowstart', type = int, default = '1')
self.arg_parser.add_argument('--bricks', type = inkex.Boolean, default = False)
self.arg_parser.add_argument('--squarebricks', type = inkex.Boolean, default = False)
self.arg_parser.add_argument('--rotate', type = inkex.Boolean, default = False)
self.arg_parser.add_argument('--coordseparator', default = '')
self.arg_parser.add_argument('--layersingroup', type = inkex.Boolean, default = False, help = 'Put all layers in a layer group.')
self.arg_parser.add_argument('--coordalphacol', type = inkex.Boolean, default = False, help = 'Reverse row coordinates.')
self.arg_parser.add_argument('--coordrevrow', type = inkex.Boolean, default = False, help = 'Reverse row coordinates.')
self.arg_parser.add_argument('--coordzeros', type = inkex.Boolean, default = True)
self.arg_parser.add_argument('--coordrowfirst', type = inkex.Boolean, default = False, help = 'Reverse row coordinates.')
self.arg_parser.add_argument('--xshift', type = inkex.Boolean, default = False, help = 'Shift grid half hex and wrap.')
self.arg_parser.add_argument('--firstcoldown', type = inkex.Boolean, default = False, help = 'Make first column half-hex down.')
self.arg_parser.add_argument('--halfhexes', type = inkex.Boolean, default = False)
self.arg_parser.add_argument('--verticesize', type = float,default = 1.0)
self.arg_parser.add_argument('--layer_grid', type = inkex.Boolean, default = True)
self.arg_parser.add_argument('--layer_fill', type = inkex.Boolean, default = True)
self.arg_parser.add_argument('--layer_coordinates', type = inkex.Boolean, default = True)
self.arg_parser.add_argument('--layer_centerdots', type = inkex.Boolean, default = True)
self.arg_parser.add_argument('--layer_vertices', type = inkex.Boolean, default = False)
self.arg_parser.add_argument('--layer_circles', type = inkex.Boolean, default = False)
def createLayer(self, name):
layer = etree.Element(inkex.addNS('g', 'svg'))
layer.set(inkex.addNS('label', 'inkscape'), name)
layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
return layer
def logwrite(self, msg):
if self.options.generatelog:
log = open(self.options.logfilepath, 'w')
log.write(msg)
log.close()
def svg_line(self, p1, p2):
line = etree.Element('line')
line.set('x1', str(p1.x + self.xoffset))
line.set('y1', str(p1.y + self.yoffset))
line.set('x2', str(p2.x + self.xoffset))
line.set('y2', str(p2.y + self.yoffset))
line.set('style', 'stroke:#000000; stroke-width:' + str(self.stroke_width) + ';stroke-linecap:round')
return line
def svg_circle(self, p, radius):
circle = etree.Element('circle')
circle.set('cx', str(p.x + self.xoffset))
circle.set('cy', str(p.y + self.yoffset))
circle.set('r', str(radius))
circle.set('fill', 'black')
return circle
def svg_polygon(self, points):
poly = etree.Element('polygon')
pointsdefa = []
for p in points:
offset_p = Point(p.x + self.xoffset, p.y + self.yoffset)
pointsdefa.append(str(offset_p))
pointsdef = ' '.join(pointsdefa)
poly.set('points', pointsdef)
poly.set('style', 'stroke:none;fill:#ffffff;fill-opacity:1;stroke-width:' + str(self.stroke_width) + ';stroke-linecap:round')
return poly
def svg_coord(self, p, col, row, cols, rows, anchor='middle'):
if self.coordrevrow:
row = rows - row
else:
row = row + 1
if self.coordrevcol:
col = cols - col
else:
col = col + 1
row = row + self.options.coordrowstart - 1
col = col + self.options.coordcolstart - 1
if ((row != 1 and row % self.coordrows != 0)
or row < 1 or col < 1):
return None
if self.coordrowfirst:
col,row = [row,col]
if self.coordalphacol:
acol = alphacol(col - 1)
if self.coordzeros:
zrow = str(row).zfill(self.rowdigits)
coord = acol + self.coordseparator + zrow
else:
coord = acol + self.coordseparator + str(row)
elif self.coordzeros:
zcol = str(col).zfill(self.coldigits)
zrow = str(row).zfill(self.rowdigits)
coord = zcol + self.coordseparator + zrow
else:
coord = str(col) + self.coordseparator + str(row)
self.logwrite(" coord-> '%s'\n" % (coord))
text = etree.Element('text')
text.set('x', str(p.x + self.xoffset))
text.set('y', str(p.y + self.yoffset))
style = ('text-align:center;text-anchor:%s;font-size:%fpt'
% (anchor, self.coordsize))
text.set('style', style)
text.text = coord
return text
def add_hexline(self, gridlayer, verticelayer, p1, p2):
if gridlayer is not None:
gridlayer.append(self.svg_line(p1, p2))
if verticelayer is not None:
verticelayer.append(self.svg_line(p1, (p2 - p1)
* self.verticesize + p1))
verticelayer.append(self.svg_line(p2, p2 - (p2 - p1)
* self.verticesize))
def effect(self):
strokewidth = self.options.strokewidth
cols = self.options.cols
rows = self.options.rows
halves = self.options.halfhexes
xshift = self.options.xshift
firstcoldown = self.options.firstcoldown
bricks = self.options.bricks
squarebricks = self.options.squarebricks
rotate = self.options.rotate
layersingroup = self.options.layersingroup
self.coordseparator = self.options.coordseparator
if self.coordseparator == None:
self.coordseparator = ''
self.coordrevrow = self.options.coordrevrow
self.coordrevcol = False
self.coordalphacol = self.options.coordalphacol
self.coordrows = self.options.coordrows
self.coordrowfirst = self.options.coordrowfirst
self.coordzeros = self.options.coordzeros
if rotate:
self.coordrowfirst = not self.coordrowfirst
self.coordrevcol = not self.coordrevrow
self.coordrevrow = False
self.verticesize = self.options.verticesize / 100.0
self.logwrite('verticesize: %f\n' % self.verticesize)
if self.verticesize < 0.01 or self.verticesize > 0.5:
self.logwrite('verticesize out of range\n')
self.verticesize = 0.15
self.coldigits = nrdigits(cols + self.options.coordcolstart)
self.rowdigits = nrdigits(rows + self.options.coordrowstart)
if self.coldigits < 2:
self.coldigits = 2
if self.rowdigits < 2:
self.rowdigits = 2
if self.coordrowfirst:
self.coldigits,self.rowdigits = [self.rowdigits,self.coldigits]
self.logwrite('cols: %d, rows: %d\n' % (cols, rows))
self.logwrite('xshift: %s, halves: %s\n' % (str(xshift), str(halves)))
svg = self.document.xpath('//svg:svg' , namespaces=NSS)[0]
self.stroke_width = self.svg.unittouu(str(self.options.strokewidth) + self.options.units)
width = float(self.svg.unittouu(svg.get('width'))) - self.stroke_width
height = float(self.svg.unittouu(svg.get('height'))) - self.stroke_width
# So I was a bit lazy and only added an offset to all the
# svg_* functions to compensate for the stroke width.
# There should be a better way.
self.xoffset = self.stroke_width * 0.5
self.yoffset = self.stroke_width * 0.5
if self.options.layer_grid:
hexgrid = self.createLayer('Hex Grid')
else:
hexgrid = None
if self.options.layer_fill:
hexfill = self.createLayer('Hex Fill')
else:
hexfill = None
if self.options.layer_coordinates:
hexcoords = self.createLayer('Hex Coordinates')
else:
hexcoords = None
if self.options.layer_centerdots:
hexdots = self.createLayer('Hex Centerdots')
else:
hexdots = None
if self.options.layer_vertices:
hexvertices = self.createLayer('Hex Vertices')
else:
hexvertices = None
if self.options.layer_circles:
hexcircles = self.createLayer('Hex Circles')
else:
hexcircles = None
if hexvertices is not None and hexgrid is not None:
hexgrid.set('style', 'display:none')
self.logwrite('w, h : %f, %f\n' % (width, height))
if xshift:
hex_cols = (cols * 3.0) * 0.25
else:
hex_cols = (cols * 3.0 + 1.0) * 0.25
if halves:
hex_rows = rows
else:
hex_rows = rows + 0.5
hex_width = width / hex_cols
if self.options.hexsize > 0:
hex_width = self.svg.unittouu(str(self.options.hexsize) + self.options.units)
hex_height = calc_hex_height(hex_width)
# square bricks workaround
if bricks and squarebricks:
hex_height = hex_width
hex_width = hex_width / 0.75
hexes_height = hex_height * hex_rows
hexes_width = hex_width * 0.75 * cols + hex_width * 0.25
self.coordsize = hex_height * COORD_SIZE_PART_OF_HEX_HEIGHT
if self.coordsize > 1.0:
self.coordsize = round(self.coordsize)
self.centerdotsize = self.stroke_width * CENTERDOT_SIZE_FACTOR
self.circlesize = hex_height / 2
self.logwrite('hex_width: %f, hex_height: %f\n' %(hex_width,
hex_height))
# FIXME try to remember what 0.005 is for
coord_yoffset = COORD_YOFFSET_PART * hex_height * 0.005
for col in range(cols + 1):
cx = (2.0 + col * 3.0) * 0.25 * hex_width
if xshift:
cx = cx - hex_width * 0.5
coldown = col % 2
if firstcoldown:
coldown = not coldown
for row in range(rows + 1):
cy = (0.5 + coldown * 0.5 + row) * hex_height
self.logwrite('col: %d, row: %d, c: %f %f\n' % (col, row,
cx, cy))
c = Point(cx, cy)
if rotate:
c = c.rotated(hexes_width)
if (hexcoords is not None
and (col < cols or xshift) and row < rows):
cc = c + Point(0, coord_yoffset)
anchor = 'middle'
if xshift and col == 0:
anchor = 'start'
elif xshift and col == cols:
anchor = 'end'
coord = self.svg_coord(cc, col, row, cols, rows, anchor)
if coord != None:
hexcoords.append(coord)
if (hexdots is not None
and (col < cols or xshift) and row < rows):
cd = self.svg_circle(c, self.centerdotsize)
cd.set('id', 'hexcenter_%d_%d'
% (col + self.options.coordcolstart,
row + self.options.coordrowstart))
hexdots.append(cd)
#FIXME make half-circles in half hexes
if (hexcircles is not None and (col < cols or xshift)
and row < rows):
el = self.svg_circle(c, self.circlesize)
el.set('id', 'hexcircle_%d_%d'
% (col + self.options.coordcolstart,
row + self.options.coordrowstart))
hexcircles.append(el)
x = [cx - hex_width * 0.5,
cx - hex_width * 0.25,
cx + hex_width * 0.25,
cx + hex_width * 0.5]
y = [cy - hex_height * 0.5,
cy,
cy + hex_height * 0.5]
if bricks and xshift:
sys.exit('No support for bricks with x shift.')
if xshift and col == 0:
x[0] = cx
x[1] = cx
elif xshift and col == cols:
x[2] = cx
x[3] = cx
if halves and coldown and row == rows-1:
y[2] = cy
# with bricks pattern, shift some coordinates a bit
# to make correct shape
if bricks:
brick_adjust = hex_width * 0.125
else:
brick_adjust = 0
p = [Point(x[2] + brick_adjust, y[0]),
Point(x[3] - brick_adjust, y[1]),
Point(x[2] + brick_adjust, y[2]),
Point(x[1] - brick_adjust, y[2]),
Point(x[0] + brick_adjust, y[1]),
Point(x[1] - brick_adjust, y[0])]
if rotate:
p = [point.rotated(hexes_width) for point in p]
if (hexfill is not None
and (col < cols or xshift) and row < rows):
if row < rows or (halves and coldown):
sp = self.svg_polygon(p)
if halves and coldown and row == rows - 1:
p2 = [x.y_mirror(hexes_height) for x in p]
sp = self.svg_polygon(p)
sp.set('id', 'hexfill_%d_%d'
% (col + self.options.coordcolstart,
row + self.options.coordrowstart))
hexfill.append(sp)
if ((col < cols and (not halves or row < rows
or not coldown))
or (xshift and col == cols
and not (halves and row == rows))):
self.add_hexline(hexgrid, hexvertices, p[5], p[0])
self.logwrite('line 0-5\n')
if row < rows:
if ((coldown or row > 0 or col < cols
or halves or xshift)
and not (xshift and col == 0)):
self.add_hexline(hexgrid, hexvertices, p[5], p[4])
self.logwrite('line 4-5\n')
if not coldown and row == 0 and col < cols:
self.add_hexline(hexgrid, hexvertices, p[0], p[1])
self.logwrite('line 0-1\n')
if not (halves and coldown and row == rows-1):
if (not (xshift and col == 0)
and not (not xshift and col == cols
and row == rows-1 and coldown)):
self.add_hexline(hexgrid, hexvertices, p[4], p[3])
self.logwrite('line 3-4\n')
if coldown and row == rows - 1 and col < cols:
self.add_hexline(hexgrid, hexvertices, p[1], p[2])
self.logwrite('line 1-2\n')
parent = svg
if layersingroup:
parent = self.createLayer('Hex Map')
self.append_if_new_name(svg, parent)
self.append_if_new_name(parent, hexfill)
self.append_if_new_name(parent, hexcircles)
self.append_if_new_name(parent, hexgrid)
self.append_if_new_name(parent, hexvertices)
self.append_if_new_name(parent, hexcoords)
self.append_if_new_name(parent, hexdots)
def append_if_new_name(self, svg, layer):
if layer is not None:
name = layer.get(inkex.addNS('label', 'inkscape'))
if not name in [c.get(inkex.addNS('label', 'inkscape'), 'name')
for c in svg.iterchildren()]:
svg.append(layer)
HexmapEffect().run()