Repository: lifelike/hexmapextension Branch: master Commit: 8faee5fe96a2 Files: 10 Total size: 42.6 KB Directory structure: gitextract_1axyjuqj/ ├── .gitignore ├── README.org ├── USER GUIDE.md ├── documentation/ │ ├── Alternate Hex Centers.md │ └── Calculated Hex Size.md ├── hexmap.inx ├── hexmap.py └── svgtests/ ├── .gitignore ├── run.py └── run.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *~ *.pyc ================================================ FILE: README.org ================================================ * Inkscape Hex Map Extension Copyright 2008-2024 Pelle Nilsson and contributors 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 ** Introduction This is an extension for creating hex grids in [[http://www.inkscape.org][Inkscape.]] It can also be used to make brick patterns of staggered rectangles. You need Inkscape 1.0 or later installed. Older versions of Inkscape are NOT supported (you can use older versions of this extensions if you must create some hexgrids using older Inkscape versions, or just upgrade Inkscape). ** Installing To install the extension you need to copy *hexmap.inx* and *hexmap.py* into the *extensions* folder (directory) of your Inkscape installation. In Linux this will typically be */usr/share/inkscape/extensions* or *~/.config/inkscape/extensions*. If Inkscape was installed on Linux from [[https://flathub.org/apps/details/org.inkscape.Inkscape][Flatpak]] the path is more likely *~/.var/app/org.inkscape.Inkscape/config/inkscape/extensions/*. In Windows it will be something like *C:\Program Files\Inkscape\share\extensions*. After (re)starting Inkscape you should now have *Boardgames* submenu in the Extensions menu, and in that an entry for running *Generate Hex Map*. ** Running The Extension Creates a grid of hexagons over the document. Up to six layers are created: - *Hex Grid* The hexgrid itself. Each hex border is a separate line object. - *Hex Centerdot* A small circle in the center of each hex, as is needed for many games. - *Hex Fill* One polygon object for each hex. Can be used to quickly add a color or pattern to hexes. - *Hex Coordinates* The coordinate label of each hex. The format of each label is controlled by parameters in the effect's dialog window. - *Hex Corners* Corners/Vertices of each hex only. Use as alternative to the normal hexgrid if you prefer a map with less visible grid. Some people call these caltrops. - *Hex Circles* Circles the size of each hex. ** User Guide There is a separate [[https://github.com/lifelike/hexmapextension/blob/master/USER%20GUIDE.md][User Guide]] document with much more information and help that is recommended reading for anyone about to use this extension or that is considering to use it. ================================================ FILE: USER GUIDE.md ================================================ # Inkscape Hex Map Extension User Guide To use the Hex Map Extension, open "Extensions" in the Inkscape top menu and select "Boardgames", "Create Hexmap…" A dialog box should open with tabs for gathering parameters and "Apply" and "Close" buttons ## Size tab ### Size Units The drop-down here sets the units (e.g. "in" or "mm") to use. ### number of rows, columns The number of rows (a row extends side-to-side) and columns (a column extends top-to-bottom) to be genrated. ### Hex Size The Hex Size field allows the user to specify the side-to-side height of the hexagon. If the hex size is 0.0, the extension fits the specified number of hexes in so that they do not overflow in any direction. The lines have non-zero thickness, and are positioned using points in the middle of the line. When the extension caluculates the size based on the page size, it moves the hex vertices in so the line thickness fits inside the page limits. Depending on line thickness, the dimension of the "open" area of the hex will be slightly or substantially different from the hex size. The details of the calculation of the hex size are given in a [separate document](documentation/Calculated%20Hex%20Size.md). If the hex size is specified, the generated hexes can overflow the page. The width of a hex is (Hex Size)/(√3/2). The length of one of the sides of a hexagon is ½*(Hex Width). ![Hexes overlaid with bricks](documentation/images/bricks-over-hexes.png?raw=true "Bricks over hexes") This example is a 5cm x 7cm page, with the extension set to create five rows and five columns. The hex size was left blank. As you can see the hexagons fill the width of the document before they fill the height. The hexagons are sized by the width of the page, and the rest of the page below the hexes is left blank. In the example the extension has been run once to create hexes, then run a second time to create "bricks" (re-colored green). Unless the "bricks" are set to be square (on the [style tab](#force-square-bricks)), the width of the "bricks" is three-quarters of the "hex size" parameter, and the "brick" height is the same as the hex height, i.e. √3/2*(Hex Size). The "bricks" are centered at the same point as a similarly-sized hex. ### Stroke width The width of the grid/vertices line segments. The stroke width also affects the size of the hex center dot, which will have a diameter of slightly more than twice the stroke width. ### Size of vertices (%) Some games have partially drawn hexsides extending from the vertices (see picture below). This parameter determines how far the hexside extends out from the vertex, with the length of the hexside being 100%. Since the partial hexside extends out from both ends, the two partial hexsides meet in the middle when the size is 50%. If the size is 50% the result is not distinguishable from the regular grid. See also the [Layers tab](#layers-tab). ![Hex map using vertices](documentation/images/ten-percent-vertices.png?raw=true "Hex map using verticies") ## Style tab ### Bricks Instead of hexagons draw rectangular "bricks." The "bricks" have their center in the same place as the hex center (unless ) ### Force Square Bricks Instead of generating rectangles with centers where the hexagons are, generate squares that are the "hex size". Note: if the hex size is calculated automatically, square "bricks" will overflow the page. ### Rotate Generate the hexes pointy sides up-and-down instead of left-and-right. The coordinates are not rotated but zig-zag up and down. The result is not the same as generating the grid normally and rotating it in Inkscape. ### Half hexes at top and bottom Generates a hex center for the half-hexes at the top and bottom. Generates a coordinate label for the half-hexes at the bottom. ### Shift grid to side and wrap Instead of beginning with the point of the first hex column on the left-hand side, begin with the center of the hex. Also end with the center of the last column. The two halves at beginning and end are considered one column when calculating the number of columns. If four columns were specified, there would be a first half column labeled "A" (alpha column labeling), three full columns labeled "B"-"D" and a final half column labeled "E". ### First column half-hex down By default the first column begins in the top left-hand of the page and the next column is shifted down a half hex. Checking this shifts the first column down a half hex instead. ## Coords tab Various ways of structuring the hex coordinates. ## Layers tab This determines which artifacts are generated. You might generate them all then turn the ones you don't need off by making them hidden in Inkscape. ### Grid The border line segments of the hexagons. ### Fill Layer containing solid hexagon fills for each hex. The fills are generated as white fills with no stroke, but can be colored or otherwise altered using Inkscape tools. If you are planning to overlay the hexagon grid over your own map, hide or don't generate this layer. ### Coordinates Layer containing the hex coordinates, structured per the [Coords tab](#coords-tab). They are positioned in the center near the bottom of the hex. You can use Inkscape tools to reposition ### Center Dots Layer containing the hex centers. These are always solid black circles. See [Alternate Hex Centers](documentation/Alternate%20Hex%20Centers.md) for steps to place different centers. ### Vertices Some games print partial hexsides rather than full hexagons to mark the grid, as shown [previously](#size-of-vertices-). This layer is a redundant with the grid layer (although you could change the color or thickness of one/both layers for some interesting effects). ### Circles Layer containing solid circles drawn with their centers where the hexagon centers are. These are generated as solid black circles with no stroke. ### Layers in Group This option generates a parent layer that contains the other generated layers. It has no visible elements itself. Having a parent layer allows the child layers to be hidden/shown together as well as making it easy to rename or delete all the generated layers. ## Debug tab If the log file field is filled in, the extension will output debug text to the file. This probably should not be filled in unless you are fixing a problem or developing a new feature. ### Generate Log File Log file is only created if this is checked. ### Log File (optional) Set this to the file to create (the button can be clicked to open a file chooser). WARNING! THIS FILE WILL BE OVERWRITTEN IF IT EXISTS (NO QUESTIONS ASKED). ## "Apply" button Generates the hexagon grid. The code will not overwrite any existing layer that match the names the extension uses. If for some reason you need to generate the hexagon grid again (e.g. you made a mistake in the parameters or you want to overlay a second grid over the first) you have to rename or delete the prior generation. ## "Close" button This button closes the Hex Map Dialog Box. ## Alternate Hex Centers The hex center created by the Hex Map Extension is a single solid black circle, the size of which is determined by the stroke width. If you need a different shape for hex centers, [here](documentation/Alternate%20Hex%20Centers.md) is a detailed list of steps that will allow you to create them. ## The Real World! The Hex Map Extension creates regular hexagons, that is all the sides and angles are the same. If you are trying to create a hexmap that is compatible with an existing printed boardgame, be aware that it may be possible that the printed map is not made of regular hexagons. One very popular board game has hexes that are around 1.06% taller than they should be if they were regular. For a single hex this is not very apparent, but the cumulative error for ten hexes (the boards for this game are ten hexes wide) is near eleven percent, which is hard to miss. The fix for this is to scale the hexgrid in one dimension using Inkscape, but the discrepancy can be very confusing if you are unaware. ================================================ FILE: documentation/Alternate Hex Centers.md ================================================ # Alternate Hex Centers The Hex Map Extension creates hex center markers as black, solid circles with a size determined by the stroke width. Simple changes to these shapes (e.g. changing color) can be made in Inkscape using standard tools. If you want to have hex centers that are different shapes (e.g. rectangles, triangles, stars, etc.), this page will show you how you can create a single object then clone it into the right positions. The procedure is not difficult, but does require some (simple) math. The procedure tries to be detailed about each step, so don't be discouraged if it looks a bit long. Make sure all measurements are in the same units. The type of units chosen will not matter because the final calculated values are percents. As long as the units are consistent any unit should produce the same answer. ## Creating a set of alternate set of center objects 1. Lock all layers except the center dots (and the group layer if you have the hex map grouped). 1. Get the size of one existing hex center by selecting it using the select tool (arrow). The width and height are given in the top toolbar. This value will be refered to as size_old_center_x (the width in the toolbar; should be the same as size_old_center_y or height because it is a circle). 1. If you specified the hex size when creating the hex grid, that is the center_distance_x. 1. If you did not specify the hex size or don't know it, you can measure it in the image. - Select two hex centers that are in adjacent columns in adjacent hexes - The width of the two selected objects minus the size_old_center_x is the center_distance_x 1. The center_distance_y = center_distance_x*√3/2 1. Create a new layer 1. Create a new hex center object on that new layer 1. Get the size of the new hex center object by selecting it, recording the size_new_center_x and size_new_center_y 1. Select the upper left existing hex center then the new hex center object. Use Inkscape to align to first object vertical and horizontal to position the new center over the old upper-left object. 1. Lock the old hex centers layer 1. Unselect the pair, then select the new hex center object 1. top menu, edit, create tiled clones - symmetry=simple translation - shift: as many rows as needed, two columns - per row: shift x=0, shift y=100*(center_distance_y - size_new_center_y)/size_new_center_y - per col: shift x=100*(center_distance_x - size_new_center_x)/size_new_center_x; shift y=100*(center_distance_y - size_new_center_y/2)/2*size_new_center_y 1. Click the "Create" button 1. [If something goes wrong, remove. Do not keep applying without removing. Bad things happen. Otherwise, at this point the first two columns should be correct.] 1. Select all the new hex center objects, original and clones 1. Record the width (group_size_x) 1. top menu, edit, create tiled clones - symmetry=simple translation - shift: one row, as many cols as needed divided by two - shift per row: shift x=0, shift y=0 - per col: shift x=100*(2*center_distance_x - group_size_x)/group_size_x; shift y=0 1. Click the "Create" button 1. Hide old hex centers, lock new hex centers ================================================ FILE: documentation/Calculated Hex Size.md ================================================ # Calculated Hex Size When the hex size parameter is left blank, the size of the hexes is calculated so the number of rows and columns will fit on the page without overflowing. The program finds the smallest dimension (height or width) page that will fit the number of rows and columns specified. The grid lines are not of zero thickness, so half a grid line thickness is subtracted from each edge (total of one grid line thickness). Thicker grid lines will result in (slightly) smaller hex sizes because of the margin to allow the full lines at the edges to fit inside the page. The remaining space is divided evenly by the number of rows or columns, depending on which would be filled first. For example, if the page size is 5 cm. by 7 cm., regular hexes will fill out the width of the page before they fill out the height. If the stroke width is 1.5 mm. (0.15 cm.), the calculated hex size will be (5 - 0.15)/5 = 1.2125 cm. ================================================ FILE: hexmap.inx ================================================ Create Hexmap pelles.effect.hexmap 7 7 30 1.000 10.0 false false false true false false sans-serif 10.0 . false false false true 1 1 1 true true true true false false false false debug.txt all ================================================ FILE: hexmap.py ================================================ #!/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) COORD_YOFFSET_PART = 75 CENTERDOT_SIZE_FACTOR = 1.1690625 # This is the ratio between the flat-to-flat size # of a hexagon vs the point-to-point size. HEX_RATIO = math.sqrt(3.0) / 2.0 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('--orientation', default = 'flattoflat') 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("--fontfamily", default='sans-serif') self.arg_parser.add_argument('--fontsize', type = float, default = 10.0) self.arg_parser.add_argument('--coordseparator', default = '') self.arg_parser.add_argument('--layersingroup', type = inkex.Boolean, default = False, help = '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 = '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: 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('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 # Apply configured start offsets (allow start values of 0) start_row = int(self.options.coordrowstart) start_col = int(self.options.coordcolstart) row = row + start_row - 1 col = col + start_col - 1 # Only create coordinates that are within the configured start # and follow the coordrows stepping. This permits start values of 0. if ((row - start_row) % self.coordrows != 0) or row < start_row or col < start_col: 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-family:"%s";font-size:%fpt;' % (anchor, self.options.fontfamily, self.options.fontsize / 3.78)) 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): if self.options.generatelog: self.log = open(self.options.logfilepath, 'w') 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 # Size-calculation here assumes un-rotated grid with # flat side up. If the grid is rotated the widths # and heights will be swapped later in the code. if self.options.hexsize > 0: hex_height = (self.svg.unittouu(str(self.options.hexsize) + self.options.units)) hex_width = hex_height / HEX_RATIO else: hex_width = width / hex_cols hex_height = hex_width * HEX_RATIO if self.options.orientation == 'peektopeek': hex_width = hex_width * HEX_RATIO hex_height = hex_height * HEX_RATIO # 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.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' if not (halves and coldown and row == rows-1): 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) if self.options.generatelog: self.log.close() 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() ================================================ FILE: svgtests/.gitignore ================================================ output ================================================ FILE: svgtests/run.py ================================================ #!/usr/bin/env python3 import difflib import glob import os import os.path import shutil import subprocess import sys def add_countersheets_paths(): sys.path.insert(0, os.getcwd()) add_countersheets_paths() inputdir = os.path.join('svgtests', 'input') outputdir = os.path.join('svgtests', 'output') expecteddir = os.path.join('svgtests', 'expected') logdir = os.path.join('svgtests', 'log') if not os.path.exists(outputdir): os.mkdir(outputdir) chosen = [a for a in sys.argv[1:] if not a.startswith("-")] GRID = ["--layer_grid=true"] FILL = ["--layer_fill=true"] COORDINATES = ["--layer_coordinates=true"] CENTERDOTS = ["--layer_centerdots=true"] VERTICES = ["--layer_vertices=true"] CIRCLES = ["--layer_circles=true"] GROUP_LAYERS = ["--layers-in-group=true"] BRICKS = ["--bricks=true"] SQUAREBRICKS = ["--squarebricks=true"] ROTATE = ["--rotate=true"] HALFHEXES= ["--halfhexes=true"] XSHIFT= ["--xshift=true"] FIRSTCOLDOWN= ["--firstcoldown=true"] DEFAULT_LAYERS = GRID + FILL + COORDINATES + CENTERDOTS ALL_LAYERS = DEFAULT_LAYERS + VERTICES + CIRCLES def cr(c, r): return ["--cols=" + str(c), "--rows=" + str(r)] def hexsize(s): return ['--hexsize=%f' % s] def units(u): return ['--units=%s' % u] def verticesize(s): return ["--verticesize=%d" % s] def strokewidth(w): return ['--strokewidth=%f' % w] def coordseparator(s): return ['--coordseparator="%s"' % s] COORDALPHACOL = ["--coordalphacol=true"] COORDREVROW = ["--coordrevrow=true"] COORDROWFIRST = ["--coordrowfirst=true"] COORDZEROS = ["--coordzeros=true"] def coordrows(r): return ["--coordrows=%d" % r] def coordcolstart(r): return ["--coordcolstart=%d" % r] def coordrowstart(r): return ["--coordrowstart=%d" % r] DEFAULTS = (ALL_LAYERS + cr(4,4) + verticesize(5) + hexsize(1) + strokewidth(1.0 / 25.4) + units("in")) tests = { "simple_1x1" : cr(1, 1) + DEFAULT_LAYERS, "all_layers_1x1" : cr(1, 1) + ALL_LAYERS, "4x4" : cr(4, 4) + ALL_LAYERS, "defaults" : DEFAULTS } successes = 0 fails = 0 skipped = 0 for name,options in tests.items(): if chosen: matches = False for c in chosen: if c in name: matches = True break if not matches: skipped += 1 continue svgoutbasename = "%s.svg" % name svgoutfile = os.path.join(outputdir, svgoutbasename) svginfile = os.path.join(inputdir, "test.svg") svgout = open(svgoutfile, "w") command = os.path.join('.', 'hexmap.py') logfile = os.path.join(logdir, 'cs_svgtests-%s.txt' % name) commandline = [command] commandline.extend(options) commandline.append(svginfile) if ['-v' in sys.argv]: print((' '.join(commandline), sys.stderr)) effect = subprocess.Popen(commandline, stdout=svgout) effect.wait() svgout.close() if effect.returncode: sys.exit('Failed to run svgtest %s.' % (name)) expectedfile = os.path.join(expecteddir, svgoutbasename) outputsvg = open(svgoutfile).read() expectedsvg = open(expectedfile).read() if outputsvg == expectedsvg: successes += 1 else: print(f"FAIL: diff {svgoutfile} {expectedfile}") fails += 1 print(f"{successes}/{len(tests)} tests OK ({skipped} skipped, {fails} FAILED)\n") ================================================ FILE: svgtests/run.sh ================================================ #!/bin/sh PYTHONPATH=/usr/share/inkscape/extensions/ svgtests/run.py "$@"