[
  {
    "path": ".gitignore",
    "content": "*~\n*.pyc\n"
  },
  {
    "path": "README.org",
    "content": "* Inkscape Hex Map Extension\n\n Copyright 2008-2024 Pelle Nilsson and contributors\n\n This program is free software; you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation; either version 2 of the License, or\n (at your option) any later version.\n\n This program is distributed in the hope that it will be useful,\n but WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n GNU General Public License for more details.\n\n You should have received a copy of the GNU General Public License\n along with this program; if not, write to the Free Software\n Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA\n\n** Introduction\nThis is an extension for creating hex grids in [[http://www.inkscape.org][Inkscape.]] It can also be used to\nmake brick patterns of staggered rectangles.\n\nYou need Inkscape 1.0 or later installed. Older versions of Inkscape\nare NOT supported (you can use older versions of this extensions\nif you must create some hexgrids using older Inkscape versions,\nor just upgrade Inkscape).\n** Installing\nTo install the extension you need to copy *hexmap.inx*\nand *hexmap.py*\ninto the *extensions* folder (directory) of your\nInkscape installation. In Linux this will typically be\n*/usr/share/inkscape/extensions*\nor *~/.config/inkscape/extensions*.\nIf Inkscape was installed on Linux from [[https://flathub.org/apps/details/org.inkscape.Inkscape][Flatpak]]\nthe path is more likely *~/.var/app/org.inkscape.Inkscape/config/inkscape/extensions/*.\nIn Windows it will be something like\n*C:\\Program Files\\Inkscape\\share\\extensions*. After (re)starting\nInkscape you should now have *Boardgames* submenu in the\nExtensions menu, and in that an entry for running\n*Generate Hex Map*.\n** Running The Extension\nCreates a grid of hexagons over the document. Up to six layers\nare created:\n\n- *Hex Grid* The hexgrid itself. Each hex border is a separate line object.\n- *Hex Centerdot* A small circle in the center of each hex, as is needed for many games.\n- *Hex Fill* One polygon object for each hex. Can be used to quickly add a color or pattern to hexes.\n- *Hex Coordinates* The coordinate label of each hex. The format of each label is controlled by parameters in the effect's dialog window.\n- *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.\n- *Hex Circles* Circles the size of each hex.\n** User Guide\nThere is a separate [[https://github.com/lifelike/hexmapextension/blob/master/USER%20GUIDE.md][User Guide]] document with much more information and help\nthat is recommended reading for anyone about to use this extension or that\nis considering to use it.\n"
  },
  {
    "path": "USER GUIDE.md",
    "content": "# Inkscape Hex Map Extension User Guide\n\nTo 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\n\n## Size tab\n\n### Size Units\nThe drop-down here sets the units (e.g. \"in\" or \"mm\") to use.\n\n### number of rows, columns\n\nThe number of rows (a row extends side-to-side) and columns (a column extends top-to-bottom) to be genrated.\n\n### Hex Size\n\nThe Hex Size field allows the user to specify the side-to-side height of the hexagon.\n\nIf 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).\n\nIf the hex size is specified, the generated hexes can overflow the page.\n\nThe width of a hex is (Hex Size)/(√3/2). The length of one of the sides of a hexagon is ½*(Hex Width).\n\n![Hexes overlaid with bricks](documentation/images/bricks-over-hexes.png?raw=true \"Bricks over hexes\")\n\nThis 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.\n\nIn 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.\n\n### Stroke width\nThe 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.\n\n### Size of vertices (%)\nSome 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).\n\n![Hex map using vertices](documentation/images/ten-percent-vertices.png?raw=true \"Hex map using verticies\")\n\n## Style tab\n\n### Bricks\nInstead of hexagons draw rectangular \"bricks.\" The \"bricks\" have their center in the same place as the hex center (unless )\n\n### Force Square Bricks\nInstead of generating rectangles with centers where the hexagons are, generate squares that are the \"hex size\". \n\nNote: if the hex size is calculated automatically, square \"bricks\" will overflow the page.\n\n### Rotate\nGenerate 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.\n\n### Half hexes at top and bottom\nGenerates a hex center for the half-hexes at the top and bottom. Generates a coordinate label for the half-hexes at the bottom.\n\n### Shift grid to side and wrap\nInstead 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\".\n\n### First column half-hex down\nBy 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.\n\n## Coords tab\n\nVarious ways of structuring the hex coordinates.\n\n## Layers tab\n\nThis 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. \n\n### Grid\nThe border line segments of the hexagons.\n\n### Fill\nLayer 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.\n\n### Coordinates\nLayer 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 \n\n### Center Dots\nLayer 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.\n\n### Vertices\nSome 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).\n\n### Circles\nLayer containing solid circles drawn with their centers where the hexagon centers are. These are generated as solid black circles with no stroke.\n\n### Layers in Group\nThis 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.\n\n\n## Debug tab\nIf 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.\n\n### Generate Log File\nLog file is only created if this is checked.\n\n### Log File (optional)\nSet 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).\n\n## \"Apply\" button\n\nGenerates 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. \n\n## \"Close\" button\n\nThis button closes the Hex Map Dialog Box.\n\n## Alternate Hex Centers\n\nThe 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. \n\n## The Real World!\n\nThe 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.\n"
  },
  {
    "path": "documentation/Alternate Hex Centers.md",
    "content": "# Alternate Hex Centers\n\nThe 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.\n\nMake 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.\n\n## Creating a set of alternate set of center objects\n1. Lock all layers except the center dots (and the group layer if you have the hex map grouped).\n1. 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).\n1. If you specified the hex size when creating the hex grid, that is the center_distance_x.\n1. If you did not specify the hex size or don't know it, you can measure it in the image. \n    - Select two hex centers that are in adjacent columns in adjacent hexes\n    - The width of the two selected objects minus the size_old_center_x is the center_distance_x\n1. The center_distance_y = center_distance_x*√3/2\n1. Create a new layer\n1. Create a new hex center object on that new layer\n1. Get the size of the new hex center object by selecting it, recording the size_new_center_x and size_new_center_y\n1. 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.\n1. Lock the old hex centers layer\n1. Unselect the pair, then select the new hex center object\n1. top menu, edit, create tiled clones\n    - symmetry=simple translation\n    - shift: as many rows as needed, two columns\n    - per row:  shift x=0, shift y=100*(center_distance_y - size_new_center_y)/size_new_center_y\n    - 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\n1. Click the \"Create\" button\n1. [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.]\n1. Select all the new hex center objects, original and clones\n1. Record the width (group_size_x)\n1. top menu, edit, create tiled clones\n    - symmetry=simple translation\n    - shift: one row, as many cols as needed divided by two\n    - shift per row:  shift x=0, shift y=0\n    - per col: shift x=100*(2*center_distance_x - group_size_x)/group_size_x; shift y=0\n1. Click the \"Create\" button\n1. Hide old hex centers, lock new hex centers\n"
  },
  {
    "path": "documentation/Calculated Hex Size.md",
    "content": "# Calculated Hex Size\n\nWhen 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.\n\nFor 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."
  },
  {
    "path": "hexmap.inx",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<inkscape-extension xmlns=\"http://www.inkscape.org/namespace/inkscape/extension\">\n    <name>Create Hexmap</name>\n    <id>pelles.effect.hexmap</id>\n    <param name=\"tab\" type=\"notebook\">\n        <page name=\"page_1\" gui-text=\"Size\">\n            <param name=\"units\" type=\"optiongroup\" appearance=\"combo\" gui-text=\"Size units\">\n               <option value=\"mm\">mm</option>\n               <option value=\"cm\">cm</option>\n               <option value=\"in\">in</option>\n               <option value=\"pt\">pt</option>\n               <option value=\"px\">px</option>\n            </param>\n            <param name=\"cols\" type=\"int\" gui-text=\"Columns\" min=\"1\" max=\"1000000\">7</param>\n            <param name=\"rows\" type=\"int\" gui-text=\"Rows\" min=\"1\" max=\"1000000\">7</param>\n            <param name=\"hexsize\" type=\"float\" min=\"0\" max=\"9999\" precision=\"2\" gui-text=\"Hex Size (optional)\">30</param>\n            <param name=\"orientation\" type=\"optiongroup\" appearance=\"combo\" gui-text=\"Hex Size Orientation\">\n               <option value=\"flattoflat\">Flat to Flat</option>\n               <option value=\"peektopeek\">Peek to Peek</option>\n            </param>\n            <param name=\"strokewidth\" type=\"float\" min=\"0.0\" max=\"9999.0\" precision=\"3\" gui-text=\"Stroke Width\">1.000</param>\n            <param name=\"verticesize\" type=\"float\" min=\"0.0\" max=\"50.0\" gui-text=\"Size of vertices (%)\">10.0</param>\n        </page>\n        <page name=\"page_2\" gui-text=\"Style\">\n            <param name=\"bricks\" type=\"bool\" gui-text=\"Bricks\">false</param>\n            <param name=\"squarebricks\" type=\"bool\" gui-text=\"Force Square Bricks\">false</param>\n            <param name=\"rotate\" type=\"bool\" gui-text=\"Rotate\">false</param>\n            <param name=\"halfhexes\" type=\"bool\" gui-text=\"Half hexes at top and bottom\">true</param>\n            <param name=\"xshift\" type=\"bool\" gui-text=\"Shift grid to side and wrap\">false</param>\n            <param name=\"firstcoldown\" type=\"bool\" gui-text=\"First column half-hex down\">false</param>\n        </page>\n        <page name=\"page_3\" gui-text=\"Coords\">\n            <param name=\"fontfamily\" type=\"string\" gui-text=\"Font Family\">sans-serif</param>\n            <param name=\"fontsize\" type=\"float\" gui-text=\"Font Size\" min=\"0\" max=\"10000\" precision=\"1\">10.0</param>\n            <param name=\"coordseparator\" type=\"string\" gui-text=\"Coordinate Separator\">.</param>\n            <param name=\"coordalphacol\" type=\"bool\" gui-text=\"Column Alpha Coordinates\">false</param>\n            <param name=\"coordrevrow\" type=\"bool\" gui-text=\"Row Coordinates Reversed\">false</param>\n            <param name=\"coordrowfirst\" type=\"bool\" gui-text=\"Row Coordinate First\">false</param>\n            <param name=\"coordzeros\" type=\"bool\" gui-text=\"Zero-Padded Coordinates\">true</param>\n            <param name=\"coordrows\" type=\"int\" min=\"1\" max=\"100\" gui-text=\"Coordinates Every ... Rows\">1</param>\n            <param name=\"coordcolstart\" type=\"int\" gui-text=\"First Col Nr\" min=\"0\" max=\"1000\">1</param>\n            <param name=\"coordrowstart\" type=\"int\" gui-text=\"First Row Nr\" min=\"0\" max=\"1000\">1</param>\n        </page>\n        <page name=\"page_4\" gui-text=\"Layers\">\n            <param name=\"layer_grid\" type=\"bool\" gui-text=\"Grid\">true</param>\n            <param name=\"layer_fill\" type=\"bool\" gui-text=\"Fill (#ffffff)\">true</param>\n            <param name=\"layer_coordinates\" type=\"bool\" gui-text=\"Coordinates\">true</param>\n            <param name=\"layer_centerdots\" type=\"bool\" gui-text=\"Centerdots\">true</param>\n            <param name=\"layer_vertices\" type=\"bool\" gui-text=\"Vertices\">false</param>\n            <param name=\"layer_circles\" type=\"bool\" gui-text=\"Circles\">false</param>\n            <param name=\"layersingroup\" type=\"bool\" gui-text=\"Layers in Group\">false</param>\n        </page>\n        <page name=\"page_5\" gui-text=\"Debug\">\n            <param name=\"generatelog\" type=\"bool\" gui-text=\"Generate log file\">false</param>\n            <param name=\"logfilepath\" type=\"path\" gui-text=\"Log File (optional)\" mode=\"file_new\" filetypes=\"txt\">debug.txt</param>\n        </page>\n    </param>\n    <effect>\n        <object-type>all</object-type>\n        <effects-menu>\n           <submenu _name=\"Boardgames\"/>\n        </effects-menu>\n    </effect>\n    <script>\n        <command location=\"inx\" interpreter=\"python\">hexmap.py</command>\n    </script>\n</inkscape-extension>\n"
  },
  {
    "path": "hexmap.py",
    "content": "#!/usr/bin/env python3\n\nimport inkex\nimport sys\nfrom inkex import NSS\nimport math\nfrom lxml import etree\n\nclass Point:\n    def __init__(self, x, y):\n        self.x = x\n        self.y = y\n\n    def __str__(self):\n        return '%f,%f' % (self.x, self.y)\n\n    def y_mirror(self, h):\n        return Point(self.x, h - self.y);\n\n    def __sub__(self, other):\n        return Point(self.x - other.x, self.y - other.y)\n\n    def __add__(self, other):\n        return Point(self.x + other.x, self.y + other.y)\n\n    def __mul__(self, k):\n        return Point(self.x * k, self.y * k)\n\n    def rotated(self, total_width):\n        return Point(self.y, total_width - self.x)\n\ndef nrdigits(f):\n    return int(math.floor(math.log10(f))) + 1\n\ndef alphacol(c):\n    d = c / 26\n    r = c % 26\n    return ('%c' % (r + 65)) * (int(d) + 1)\n\nCOORD_YOFFSET_PART = 75\nCENTERDOT_SIZE_FACTOR = 1.1690625\n\n# This is the ratio between the flat-to-flat size\n# of a hexagon vs the point-to-point size.\nHEX_RATIO = math.sqrt(3.0) / 2.0\n\nclass HexmapEffect(inkex.Effect):\n    def __init__(self):\n        inkex.Effect.__init__(self)\n        self.arg_parser.add_argument('--tab')\n        self.arg_parser.add_argument('--generatelog', type = inkex.Boolean,\n                                         default = False)\n        self.arg_parser.add_argument('--logfilepath', default = \"debug.txt\")\n        self.arg_parser.add_argument(\"--units\", default='mm',\n                                         help=\"Units this dialog is using\")\n        self.arg_parser.add_argument('--cols', type = int, default = '10',\n                                         help = 'Number of columns')\n        self.arg_parser.add_argument('--rows', type = int, default = '10',\n                                         help = 'Number of columns')\n        self.arg_parser.add_argument('--hexsize', type = float, default = 0.0)\n        self.arg_parser.add_argument('--orientation', default = 'flattoflat')\n        self.arg_parser.add_argument('--strokewidth', type = float,\n                                         default = 1.0)\n        self.arg_parser.add_argument('--coordrows', type = int, default = '1')\n        self.arg_parser.add_argument('--coordcolstart', type = int,\n                                         default = '1')\n        self.arg_parser.add_argument('--coordrowstart', type = int,\n                                         default = '1')\n        self.arg_parser.add_argument('--bricks', type = inkex.Boolean,\n                                         default = False)\n        self.arg_parser.add_argument('--squarebricks', type = inkex.Boolean,\n                                         default = False)\n        self.arg_parser.add_argument('--rotate', type = inkex.Boolean,\n                                         default = False)\n        self.arg_parser.add_argument(\"--fontfamily\", default='sans-serif')\n        self.arg_parser.add_argument('--fontsize', type = float,\n                                         default = 10.0)\n        self.arg_parser.add_argument('--coordseparator', default = '')\n        self.arg_parser.add_argument('--layersingroup', type = inkex.Boolean,\n                                         default = False,\n                                         help = 'All layers in a layer group')\n        self.arg_parser.add_argument('--coordalphacol', type = inkex.Boolean,\n                                         default = False,\n                                         help = 'Reverse row coordinates')\n        self.arg_parser.add_argument('--coordrevrow', type = inkex.Boolean,\n                                         default = False,\n                                         help = 'Reverse row coordinates')\n        self.arg_parser.add_argument('--coordzeros', type = inkex.Boolean,\n                                         default = True)\n        self.arg_parser.add_argument('--coordrowfirst', type = inkex.Boolean,\n                                         default = False,\n                                         help = 'Reverse row coordinates')\n        self.arg_parser.add_argument('--xshift', type = inkex.Boolean,\n                                         default = False,\n                                         help = 'Shift grid half hex and wrap')\n        self.arg_parser.add_argument('--firstcoldown', type = inkex.Boolean,\n                                         default = False,\n                                         help = 'First column half-hex down')\n        self.arg_parser.add_argument('--halfhexes', type = inkex.Boolean,\n                                         default = False)\n        self.arg_parser.add_argument('--verticesize', type = float,\n                                         default = 1.0)\n        self.arg_parser.add_argument('--layer_grid', type = inkex.Boolean,\n                                         default = True)\n        self.arg_parser.add_argument('--layer_fill', type = inkex.Boolean,\n                                         default = True)\n        self.arg_parser.add_argument('--layer_coordinates',\n                                         type = inkex.Boolean, default = True)\n        self.arg_parser.add_argument('--layer_centerdots',\n                                         type = inkex.Boolean, default = True)\n        self.arg_parser.add_argument('--layer_vertices',\n                                         type = inkex.Boolean, default = False)\n        self.arg_parser.add_argument('--layer_circles',\n                                         type = inkex.Boolean, default = False)\n\n    def createLayer(self, name):\n        layer = etree.Element(inkex.addNS('g', 'svg'))\n        layer.set(inkex.addNS('label', 'inkscape'), name)\n        layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')\n        return layer\n\n    def logwrite(self, msg):\n        if self.options.generatelog:\n            self.log.write(msg)\n\n    def svg_line(self, p1, p2):\n        line = etree.Element('line')\n        line.set('x1', str(p1.x + self.xoffset))\n        line.set('y1', str(p1.y + self.yoffset))\n        line.set('x2', str(p2.x + self.xoffset))\n        line.set('y2', str(p2.y + self.yoffset))\n        line.set('style', 'stroke:#000000; stroke-width:'\n                 + str(self.stroke_width) + ';stroke-linecap:round')\n        return line\n\n    def svg_circle(self, p, radius):\n        circle = etree.Element('circle')\n        circle.set('cx', str(p.x + self.xoffset))\n        circle.set('cy', str(p.y + self.yoffset))\n        circle.set('r', str(radius))\n        circle.set('fill', 'black')\n        return circle\n\n    def svg_polygon(self, points):\n        poly = etree.Element('polygon')\n        pointsdefa = []\n        for p in points:\n            offset_p = Point(p.x + self.xoffset, p.y + self.yoffset)\n            pointsdefa.append(str(offset_p))\n        pointsdef = ' '.join(pointsdefa)\n        poly.set('points', pointsdef)\n        poly.set('style',\n                     'stroke:none;fill:#ffffff;fill-opacity:1;stroke-width:'\n                     + str(self.stroke_width) + ';stroke-linecap:round')\n        return poly\n\n    def svg_coord(self, p, col, row, cols, rows, anchor='middle'):\n        if self.coordrevrow:\n            row = rows - row\n        else:\n            row = row + 1\n        if self.coordrevcol:\n            col = cols - col\n        else:\n            col = col + 1\n        # Apply configured start offsets (allow start values of 0)\n        start_row = int(self.options.coordrowstart)\n        start_col = int(self.options.coordcolstart)\n        row = row + start_row - 1\n        col = col + start_col - 1\n\n        # Only create coordinates that are within the configured start\n        # and follow the coordrows stepping. This permits start values of 0.\n        if ((row - start_row) % self.coordrows != 0) or row < start_row or col < start_col:\n            return None\n\n        if self.coordrowfirst:\n             col,row = [row,col]\n\n        if self.coordalphacol:\n            acol = alphacol(col - 1)\n            if self.coordzeros:\n                zrow = str(row).zfill(self.rowdigits)\n                coord = acol + self.coordseparator + zrow\n            else:\n                coord = acol + self.coordseparator + str(row)\n        elif self.coordzeros:\n            zcol = str(col).zfill(self.coldigits)\n            zrow = str(row).zfill(self.rowdigits)\n            coord = zcol + self.coordseparator + zrow\n        else:\n            coord = str(col) + self.coordseparator + str(row)\n\n        self.logwrite(\" coord-> '%s'\\n\" % (coord))\n        text = etree.Element('text')\n        text.set('x', str(p.x + self.xoffset))\n        text.set('y', str(p.y + self.yoffset))\n        style = ('text-align:center;text-anchor:%s;font-family:\"%s\";font-size:%fpt;'\n                 % (anchor, self.options.fontfamily, self.options.fontsize / 3.78))\n        text.set('style', style)\n        text.text = coord\n        return text\n\n    def add_hexline(self, gridlayer, verticelayer, p1, p2):\n        if gridlayer is not None:\n            gridlayer.append(self.svg_line(p1, p2))\n        if verticelayer is not None:\n            verticelayer.append(self.svg_line(p1, (p2 - p1)\n                                            * self.verticesize + p1))\n            verticelayer.append(self.svg_line(p2, p2 - (p2 - p1)\n                                            * self.verticesize))\n\n    def effect(self):\n        if self.options.generatelog:\n            self.log = open(self.options.logfilepath, 'w')\n\n        strokewidth = self.options.strokewidth\n        cols = self.options.cols\n        rows = self.options.rows\n        halves = self.options.halfhexes\n        xshift = self.options.xshift\n        firstcoldown = self.options.firstcoldown\n        bricks = self.options.bricks\n        squarebricks = self.options.squarebricks\n        rotate = self.options.rotate\n        layersingroup = self.options.layersingroup\n\n        self.coordseparator = self.options.coordseparator\n        if self.coordseparator == None:\n            self.coordseparator = ''\n        self.coordrevrow = self.options.coordrevrow\n        self.coordrevcol = False\n        self.coordalphacol = self.options.coordalphacol\n        self.coordrows = self.options.coordrows\n        self.coordrowfirst = self.options.coordrowfirst\n        self.coordzeros = self.options.coordzeros\n\n        if rotate:\n            self.coordrowfirst = not self.coordrowfirst\n            self.coordrevcol = not self.coordrevrow\n            self.coordrevrow = False\n\n        self.verticesize = self.options.verticesize / 100.0\n        self.logwrite('verticesize: %f\\n' % self.verticesize)\n        if self.verticesize < 0.01 or self.verticesize > 0.5:\n            self.logwrite('verticesize out of range\\n')\n            self.verticesize = 0.15\n\n        self.coldigits = nrdigits(cols + self.options.coordcolstart)\n        self.rowdigits = nrdigits(rows + self.options.coordrowstart)\n        if self.coldigits < 2:\n            self.coldigits = 2\n        if self.rowdigits < 2:\n            self.rowdigits = 2\n        if self.coordrowfirst:\n            self.coldigits,self.rowdigits = [self.rowdigits,self.coldigits]\n\n        self.logwrite('cols: %d, rows: %d\\n' % (cols, rows))\n        self.logwrite('xshift: %s, halves: %s\\n' % (str(xshift), str(halves)))\n\n        svg = self.document.xpath('//svg:svg' , namespaces=NSS)[0]\n\n        self.stroke_width = self.svg.unittouu(str(self.options.strokewidth)\n                                                  + self.options.units)\n\n        width = (float(self.svg.unittouu(svg.get('width')))\n                  - self.stroke_width)\n        height = (float(self.svg.unittouu(svg.get('height')))\n                  - self.stroke_width)\n\n        # So I was a bit lazy and only added an offset to all the\n        # svg_* functions to compensate for the stroke width.\n        # There should be a better way.\n        self.xoffset = self.stroke_width * 0.5\n        self.yoffset = self.stroke_width * 0.5\n\n        if self.options.layer_grid:\n            hexgrid = self.createLayer('Hex Grid')\n        else:\n            hexgrid = None\n        if self.options.layer_fill:\n            hexfill = self.createLayer('Hex Fill')\n        else:\n            hexfill = None\n        if self.options.layer_coordinates:\n            hexcoords = self.createLayer('Hex Coordinates')\n        else:\n            hexcoords = None\n        if self.options.layer_centerdots:\n            hexdots = self.createLayer('Hex Centerdots')\n        else:\n            hexdots = None\n        if self.options.layer_vertices:\n            hexvertices = self.createLayer('Hex Vertices')\n        else:\n            hexvertices = None\n        if self.options.layer_circles:\n            hexcircles = self.createLayer('Hex Circles')\n        else:\n            hexcircles = None\n        if hexvertices is not None and hexgrid is not None:\n            hexgrid.set('style', 'display:none')\n\n        self.logwrite('w, h : %f, %f\\n' % (width, height))\n\n        if xshift:\n            hex_cols = (cols * 3.0) * 0.25\n        else:\n            hex_cols = (cols * 3.0 + 1.0) * 0.25\n\n        if halves:\n            hex_rows = rows\n        else:\n            hex_rows = rows + 0.5\n\n        # Size-calculation here assumes un-rotated grid with\n        # flat side up. If the grid is rotated the widths\n        # and heights will be swapped later in the code.\n        if self.options.hexsize > 0:\n            hex_height = (self.svg.unittouu(str(self.options.hexsize)\n                                               + self.options.units))\n            hex_width = hex_height / HEX_RATIO\n        else:\n            hex_width = width / hex_cols\n            hex_height = hex_width * HEX_RATIO\n\n        if self.options.orientation == 'peektopeek':\n            hex_width = hex_width * HEX_RATIO\n            hex_height = hex_height * HEX_RATIO\n\n        # square bricks workaround\n        if bricks and squarebricks:\n            hex_height = hex_width\n            hex_width = hex_width / 0.75\n\n        hexes_height = hex_height * hex_rows\n        hexes_width = hex_width * 0.75 * cols + hex_width * 0.25\n\n        self.centerdotsize = self.stroke_width * CENTERDOT_SIZE_FACTOR\n        self.circlesize = hex_height / 2\n\n        self.logwrite('hex_width: %f, hex_height: %f\\n' %(hex_width,\n                                                          hex_height))\n\n        # FIXME try to remember what 0.005 is for\n        coord_yoffset = COORD_YOFFSET_PART * hex_height * 0.005\n\n        for col in range(cols + 1):\n            cx = (2.0 + col * 3.0) * 0.25 * hex_width\n            if xshift:\n                cx = cx - hex_width * 0.5\n            coldown = col % 2\n            if firstcoldown:\n                coldown = not coldown\n            for row in range(rows + 1):\n                cy = (0.5 + coldown * 0.5 + row) * hex_height\n                self.logwrite('col: %d, row: %d, c: %f %f\\n' % (col, row,\n                                                                cx, cy))\n                c = Point(cx, cy)\n                if rotate:\n                    c = c.rotated(hexes_width)\n                if (hexcoords is not None\n                    and (col < cols or xshift) and row < rows):\n                    cc = c + Point(0, coord_yoffset)\n                    anchor = 'middle'\n                    if xshift and col == 0:\n                        anchor = 'start'\n                    elif xshift and col == cols:\n                        anchor = 'end'\n                    if not (halves and coldown and row == rows-1):\n                        coord = self.svg_coord(cc, col, row, cols, rows, anchor)\n                    if coord != None:\n                        hexcoords.append(coord)\n                if (hexdots is not None\n                    and (col < cols or xshift) and row < rows):\n                    cd = self.svg_circle(c, self.centerdotsize)\n                    cd.set('id', 'hexcenter_%d_%d'\n                           % (col + self.options.coordcolstart,\n                              row + self.options.coordrowstart))\n                    hexdots.append(cd)\n                #FIXME make half-circles in half hexes\n                if (hexcircles is not None and (col < cols or xshift)\n                    and row < rows):\n                    el = self.svg_circle(c, self.circlesize)\n                    el.set('id', 'hexcircle_%d_%d'\n                           % (col + self.options.coordcolstart,\n                              row + self.options.coordrowstart))\n                    hexcircles.append(el)\n                x = [cx - hex_width * 0.5,\n                     cx - hex_width * 0.25,\n                     cx + hex_width * 0.25,\n                     cx + hex_width * 0.5]\n                y = [cy - hex_height * 0.5,\n                     cy,\n                     cy + hex_height * 0.5]\n                if bricks and xshift:\n                    sys.exit('No support for bricks with x shift.')\n                if xshift and col == 0:\n                    x[0] = cx\n                    x[1] = cx\n                elif xshift and col == cols:\n                    x[2] = cx\n                    x[3] = cx\n                if halves and coldown and row == rows-1:\n                    y[2] = cy\n                # with bricks pattern, shift some coordinates a bit\n                # to make correct shape\n                if bricks:\n                    brick_adjust = hex_width * 0.125\n                else:\n                    brick_adjust = 0\n                p = [Point(x[2] + brick_adjust, y[0]),\n                     Point(x[3] - brick_adjust, y[1]),\n                     Point(x[2] + brick_adjust, y[2]),\n                     Point(x[1] - brick_adjust, y[2]),\n                     Point(x[0] + brick_adjust, y[1]),\n                     Point(x[1] - brick_adjust, y[0])]\n                if rotate:\n                    p = [point.rotated(hexes_width) for point in p]\n                if (hexfill is not None\n                    and (col < cols or xshift) and row < rows):\n                    if row < rows or (halves and coldown):\n                        sp = self.svg_polygon(p)\n                    if halves and coldown and row == rows - 1:\n                        p2 = [x.y_mirror(hexes_height) for x in p]\n                        sp = self.svg_polygon(p)\n                    sp.set('id', 'hexfill_%d_%d'\n                           % (col + self.options.coordcolstart,\n                              row + self.options.coordrowstart))\n                    hexfill.append(sp)\n                if ((col < cols and (not halves or row < rows\n                                     or not coldown))\n                    or (xshift and col == cols\n                        and not (halves and row == rows))):\n                    self.add_hexline(hexgrid, hexvertices, p[5], p[0])\n                    self.logwrite('line 0-5\\n')\n                if row < rows:\n                    if ((coldown or row > 0 or col < cols\n                         or halves or xshift)\n                        and not (xshift and col == 0)):\n                        self.add_hexline(hexgrid, hexvertices, p[5], p[4])\n                        self.logwrite('line 4-5\\n')\n                    if not coldown and row == 0 and col < cols:\n                        self.add_hexline(hexgrid, hexvertices, p[0], p[1])\n                        self.logwrite('line 0-1\\n')\n                    if not (halves and coldown and row == rows-1):\n                        if (not (xshift and col == 0)\n                            and not (not xshift and col == cols\n                                     and row == rows-1 and coldown)):\n                            self.add_hexline(hexgrid, hexvertices, p[4], p[3])\n                            self.logwrite('line 3-4\\n')\n                        if coldown and row == rows - 1 and col < cols:\n                            self.add_hexline(hexgrid, hexvertices, p[1], p[2])\n                            self.logwrite('line 1-2\\n')\n        parent = svg\n        if layersingroup:\n            parent = self.createLayer('Hex Map')\n            self.append_if_new_name(svg, parent)\n        self.append_if_new_name(parent, hexfill)\n        self.append_if_new_name(parent, hexcircles)\n        self.append_if_new_name(parent, hexgrid)\n        self.append_if_new_name(parent, hexvertices)\n        self.append_if_new_name(parent, hexcoords)\n        self.append_if_new_name(parent, hexdots)\n\n        if self.options.generatelog:\n            self.log.close()\n\n    def append_if_new_name(self, svg, layer):\n        if layer is not None:\n            name = layer.get(inkex.addNS('label', 'inkscape'))\n            if not name in [c.get(inkex.addNS('label', 'inkscape'), 'name')\n                            for c in svg.iterchildren()]:\n                svg.append(layer)\n\nHexmapEffect().run()\n"
  },
  {
    "path": "svgtests/.gitignore",
    "content": "output\n"
  },
  {
    "path": "svgtests/run.py",
    "content": "#!/usr/bin/env python3\n\nimport difflib\nimport glob\nimport os\nimport os.path\nimport shutil\nimport subprocess\nimport sys\n\ndef add_countersheets_paths():\n    sys.path.insert(0, os.getcwd())\n\nadd_countersheets_paths()\n\ninputdir = os.path.join('svgtests', 'input')\noutputdir = os.path.join('svgtests', 'output')\nexpecteddir = os.path.join('svgtests', 'expected')\nlogdir = os.path.join('svgtests', 'log')\n\nif not os.path.exists(outputdir):\n    os.mkdir(outputdir)\n\nchosen = [a for a in sys.argv[1:]\n          if not a.startswith(\"-\")]\n\nGRID = [\"--layer_grid=true\"]\nFILL = [\"--layer_fill=true\"]\nCOORDINATES = [\"--layer_coordinates=true\"]\nCENTERDOTS = [\"--layer_centerdots=true\"]\nVERTICES = [\"--layer_vertices=true\"]\nCIRCLES = [\"--layer_circles=true\"]\n\nGROUP_LAYERS = [\"--layers-in-group=true\"]\n\nBRICKS = [\"--bricks=true\"]\nSQUAREBRICKS = [\"--squarebricks=true\"]\nROTATE = [\"--rotate=true\"]\nHALFHEXES= [\"--halfhexes=true\"]\nXSHIFT= [\"--xshift=true\"]\nFIRSTCOLDOWN= [\"--firstcoldown=true\"]\n\nDEFAULT_LAYERS = GRID + FILL + COORDINATES + CENTERDOTS\nALL_LAYERS = DEFAULT_LAYERS + VERTICES + CIRCLES\n\ndef cr(c, r):\n    return [\"--cols=\" + str(c),\n            \"--rows=\" + str(r)]\n\ndef hexsize(s):\n    return ['--hexsize=%f' % s]\n\ndef units(u):\n    return ['--units=%s' % u]\n\ndef verticesize(s):\n    return [\"--verticesize=%d\" % s]\n\ndef strokewidth(w):\n    return ['--strokewidth=%f' % w]\n\ndef coordseparator(s):\n    return ['--coordseparator=\"%s\"' % s]\n\nCOORDALPHACOL = [\"--coordalphacol=true\"]\nCOORDREVROW = [\"--coordrevrow=true\"]\nCOORDROWFIRST = [\"--coordrowfirst=true\"]\nCOORDZEROS = [\"--coordzeros=true\"]\n\ndef coordrows(r):\n    return [\"--coordrows=%d\" % r]\ndef coordcolstart(r):\n    return [\"--coordcolstart=%d\" % r]\ndef coordrowstart(r):\n    return [\"--coordrowstart=%d\" % r]\n\nDEFAULTS = (ALL_LAYERS\n                + cr(4,4)\n                + verticesize(5)\n                + hexsize(1)\n                + strokewidth(1.0 / 25.4)\n                + units(\"in\"))\n\ntests = {\n    \"simple_1x1\" : cr(1, 1) + DEFAULT_LAYERS,\n    \"all_layers_1x1\" : cr(1, 1) + ALL_LAYERS,\n    \"4x4\" : cr(4, 4) + ALL_LAYERS,\n    \"defaults\" : DEFAULTS\n    }\n\nsuccesses = 0\nfails = 0\nskipped = 0\nfor name,options in tests.items():\n    if chosen:\n        matches = False\n        for c in chosen:\n            if c in name:\n                matches = True\n                break\n        if not matches:\n            skipped += 1\n            continue\n    svgoutbasename = \"%s.svg\" % name\n    svgoutfile = os.path.join(outputdir, svgoutbasename)\n    svginfile = os.path.join(inputdir, \"test.svg\")\n    svgout = open(svgoutfile, \"w\")\n    command = os.path.join('.', 'hexmap.py')\n    logfile = os.path.join(logdir, 'cs_svgtests-%s.txt' % name)\n    commandline = [command]\n    commandline.extend(options)\n    commandline.append(svginfile)\n\n    if ['-v' in sys.argv]:\n        print((' '.join(commandline), sys.stderr))\n\n    effect = subprocess.Popen(commandline,\n                              stdout=svgout)\n    effect.wait()\n    svgout.close()\n    if effect.returncode:\n        sys.exit('Failed to run svgtest %s.'\n                 % (name))\n\n    expectedfile = os.path.join(expecteddir, svgoutbasename)\n    outputsvg = open(svgoutfile).read()\n    expectedsvg = open(expectedfile).read()\n    if outputsvg == expectedsvg:\n        successes += 1\n    else:\n        print(f\"FAIL: diff {svgoutfile} {expectedfile}\")\n        fails += 1\n\nprint(f\"{successes}/{len(tests)} tests OK ({skipped} skipped, {fails} FAILED)\\n\")\n"
  },
  {
    "path": "svgtests/run.sh",
    "content": "#!/bin/sh\n\nPYTHONPATH=/usr/share/inkscape/extensions/ svgtests/run.py \"$@\"\n"
  }
]