From a282b316b62fa62560b32630bd8c0e5dbe086635 Mon Sep 17 00:00:00 2001 From: Mario Voigt Date: Mon, 17 Aug 2020 18:11:51 +0200 Subject: [PATCH] Added hexmap extension --- .../fablabchemnitz_attributes_import.inx | 2 +- extensions/fablabchemnitz_hexmap.inx | 63 +++ extensions/fablabchemnitz_hexmap.py | 419 ++++++++++++++++++ 3 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 extensions/fablabchemnitz_hexmap.inx create mode 100644 extensions/fablabchemnitz_hexmap.py diff --git a/extensions/fablabchemnitz_attributes_import.inx b/extensions/fablabchemnitz_attributes_import.inx index 606ee95c..03dfffb1 100644 --- a/extensions/fablabchemnitz_attributes_import.inx +++ b/extensions/fablabchemnitz_attributes_import.inx @@ -5,7 +5,7 @@ Uses lines in text file to edit attributes of elements. Line: 'elementID,attributeName,attributeValue'. For namespaces use {namespaceUrl}attributeName - + all diff --git a/extensions/fablabchemnitz_hexmap.inx b/extensions/fablabchemnitz_hexmap.inx new file mode 100644 index 00000000..57c89923 --- /dev/null +++ b/extensions/fablabchemnitz_hexmap.inx @@ -0,0 +1,63 @@ + + + Create Hexmap + fablabchemnitz.de.hexmap + + + + + + + + + + 10 + 10 + + 1.0 + 10.0 + + + false + false + false + false + false + false + + + . + false + false + false + true + 1 + 1 + 1 + + + true + true + true + true + false + false + false + + + false + debug.txt + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz_hexmap.py b/extensions/fablabchemnitz_hexmap.py new file mode 100644 index 00000000..53d2f97b --- /dev/null +++ b/extensions/fablabchemnitz_hexmap.py @@ -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() \ No newline at end of file