Last active
January 12, 2020 12:25
-
-
Save will-hart/9e8b9ebd306427e3f0209f14c1d5bc06 to your computer and use it in GitHub Desktop.
Hexmap generator inkscape extension, without inkscape (and using python 3)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# borrowed from https://raw.githubusercontent.com/lifelike/hexmapextension/master/hexmap.py | |
# which isn't working in inkscape | |
# import inkex | |
import sys | |
# from inkex import NSS | |
import math | |
import lxml | |
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)) * (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 | |
LAYERS = ["grid", "centerdots", "vertices", "fill", "coordinates", "circles"] | |
class Options: | |
svgwidth = 2000 | |
svgheight = 1000 | |
cols = 20 | |
rows = 10 | |
hexsize = "" | |
strokewidth = 1 | |
coordrows = 1 | |
coordcolstart = 1 | |
coordrowstart = 1 | |
bricks = False | |
squarebricks = "" | |
rotate = False | |
coordseparator = "." | |
layersingroup = False | |
coordalphacol = True | |
coordrevrow = False | |
coordzeros = True | |
coordrowfirst = False | |
xshift = False | |
firstcoldown = False | |
halfhexes = False | |
verticesize = 30 | |
layergrid = True | |
layercenterdots = True | |
layervertices = True | |
layerfill = False | |
layercoordinates = True | |
layercircles = False | |
logfile = "./log.txt" | |
class HexmapEffect: | |
def __init__(self): | |
self.options = Options() | |
self.log = None | |
def createLayer(self, name): | |
layer = etree.Element('g') | |
layer.set('id', name) | |
return layer | |
def logwrite(self, msg): | |
if not self.log and self.options.logfile: | |
self.log = open(self.options.logfile, 'w') | |
if self.log: | |
self.log.write(msg) | |
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("stroke", "black") | |
line.set("stroke-width", str(self.stroke_width)) | |
line.set("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") | |
poly.set("stroke-width", str(self.stroke_width)) | |
poly.set("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 == "true" | |
xshift = self.options.xshift == "true" | |
firstcoldown = self.options.firstcoldown == "true" | |
bricks = self.options.bricks == "true" | |
squarebricks = self.options.squarebricks == "true" | |
rotate = self.options.rotate == "true" | |
layersingroup = self.options.layersingroup == "true" | |
self.coordseparator = self.options.coordseparator | |
if self.coordseparator == None: | |
self.coordseparator = "" | |
self.coordrevrow = self.options.coordrevrow == "true" | |
self.coordrevcol = False | |
self.coordalphacol = self.options.coordalphacol == "true" | |
self.coordrows = self.options.coordrows | |
self.coordrowfirst = self.options.coordrowfirst == "true" | |
self.coordzeros = self.options.coordzeros == "true" | |
self.enabled_layers = set() | |
for layer in LAYERS: | |
if getattr(self.options, f'layer{layer}'): | |
self.enabled_layers.add(layer) | |
if len(self.enabled_layers) == 0: | |
raise ValueError("No layers are enabled.") | |
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 = etree.Element("svg") # self.document.xpath('//svg:svg' , namespaces=NSS)[0] | |
self.stroke_width = self.parse_float_with_unit( | |
self.options.strokewidth, "stroke width") | |
width = self.options.svgwidth - self.stroke_width # float(self.unittouu(svg.get('width'))) - self.stroke_width | |
height = self.options.svgheight - self.stroke_width # float(self.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 | |
# FIXME there is room for improvement here | |
if 'grid' in self.enabled_layers: | |
hexgrid = self.createLayer("Hex Grid") | |
else: | |
hexgrid = None | |
if 'centerdots' in self.enabled_layers: | |
hexdots = self.createLayer("Hex Centerdots") | |
else: | |
hexdots = None | |
if 'vertices' in self.enabled_layers: | |
hexvertices = self.createLayer("Hex Vertices") | |
else: | |
hexvertices = None | |
if 'fill' in self.enabled_layers: | |
hexfill = self.createLayer("Hex Fill") | |
else: | |
hexfill = None | |
if 'coordinates' in self.enabled_layers: | |
hexcoords = self.createLayer("Hex Coordinates") | |
else: | |
hexcoords = None | |
if 'circles' in self.enabled_layers: | |
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 and self.options.hexsize != "": | |
hex_width = self.parse_float_with_unit(self.options.hexsize, | |
"hex size") | |
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) | |
if hexfill is not None: | |
self.append_if_new_name(parent, hexfill) | |
if hexcircles is not None: | |
self.append_if_new_name(parent, hexcircles) | |
if hexgrid is not None: | |
self.append_if_new_name(parent, hexgrid) | |
if hexvertices is not None: | |
self.append_if_new_name(parent, hexvertices) | |
if hexcoords is not None: | |
self.append_if_new_name(parent, hexcoords) | |
if hexdots is not None: | |
self.append_if_new_name(parent, hexdots) | |
with open('test.svg', 'wb') as f: | |
f.write(etree.tostring(parent, pretty_print=True)) | |
def parse_float_with_unit(self, value, name): | |
try: | |
result = hex_width = value # self.unittouu(value.strip()) | |
except: | |
sys.exit("Failed to parse %s '%s'. Must be " | |
"digits followed by optional unit name " | |
"(e.g. mm, cm, in, pt). If no unit is " | |
"named the default will be whatever the " | |
"default units for the document is." | |
% (name, value)) | |
if result <= 0: | |
sys.exit("%s must be positive" % name) | |
return result | |
def append_if_new_name(self, svg, layer): | |
svg.append(layer) | |
if __name__ == "__main__": | |
effect = HexmapEffect() | |
effect.effect() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
GPL | |
see https://github.com/lifelike/hexmapextension |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment