weavingspace.tiling_geometries

Functions for setting up a weavingspace.tile_unit.TileUnit with various tile geometries. Some care is required in adding new functions that use exisitng ones to get the sequence of setup operations right. Modify with care!

The available tilings can be viewed in this notebook.

These tilings (and many many more!) are discussed in

Grunbaum B, Shephard G C, 1987 _Tilings and Patterns_ (W. H. Freeman and Company, New York)

A more accessible introduction is

Kaplan C S, 2002 _Computer Graphics and Geometric Ornamental Design_, PhD thesis, University of Washington, Seattle, WA, https://cs.uwaterloo.ca/~csk/other/phd/kaplan_diss_full_print.pdf

and a more 'polished' version of that work focused on computer graphics is also available

Kaplan C S, 2009 _Introductory tiling theory for computer graphics_ (Morgan & Claypool)

  1#!/usr/bin/env python
  2# coding: utf-8
  3
  4"""Functions for setting up a `weavingspace.tile_unit.TileUnit` with various
  5tile geometries. Some care is required in adding new functions that use
  6exisitng ones to get the sequence of setup operations right. Modify with care!
  7
  8The available tilings can be viewed in [this notebook](https://github.com/DOSull/weaving-space/blob/main/all-the-tiles.ipynb).
  9
 10These tilings (and many many more!) are discussed in
 11
 12Grunbaum B, Shephard G C, 1987 _Tilings and Patterns_ (W. H. Freeman and
 13Company, New York)
 14
 15A more accessible introduction is
 16
 17Kaplan C S, 2002 _Computer Graphics and Geometric Ornamental Design_, PhD
 18thesis, University of Washington, Seattle, WA, https://cs.uwaterloo.ca/~csk/other/phd/kaplan_diss_full_print.pdf
 19
 20and a more 'polished' version of that work focused on computer graphics is also
 21available
 22
 23Kaplan C S, 2009 _Introductory tiling theory for computer graphics_ (Morgan &
 24Claypool)
 25"""
 26
 27from typing import TYPE_CHECKING
 28import copy
 29import itertools
 30import string
 31
 32import geopandas as gpd
 33import numpy as np
 34import shapely.geometry as geom
 35import shapely.affinity as affine
 36
 37from weavingspace.tileable import TileShape
 38from weavingspace.tile_unit import TileUnit
 39import weavingspace.tiling_utils as tiling_utils
 40
 41def _setup_none_tile(unit:TileUnit) -> None:
 42  """Setups a 'null' tile unit with one tile and one tile_id.
 43
 44  Args:
 45    unit (TileUnit): the TileUnit to setup.
 46  """
 47  _setup_base_tile(unit, unit.base_shape)
 48  unit.tiles = gpd.GeoDataFrame(
 49    data = {"tile_id": ["a"]}, crs = unit.crs,
 50    geometry = copy.deepcopy(unit.prototile.geometry))
 51  return
 52
 53
 54def _setup_base_tile(unit:TileUnit, shape:TileShape) -> None:
 55  """_summary_
 56
 57  Args:
 58    unit (TileUnit):  the TileUnit to setup.
 59    shape (TileShape): the TileShape to apply.
 60  """
 61  unit.base_shape = shape
 62  if unit.base_shape == TileShape.DIAMOND:
 63    tile = tiling_utils.gridify(geom.Polygon([
 64      (unit.spacing / 2, 0), (0, unit.spacing * np.sqrt(3) / 2),
 65      (-unit.spacing / 2, 0), (0, -unit.spacing * np.sqrt(3) / 2)]))
 66  else:
 67    tile = tiling_utils.get_regular_polygon(
 68      unit.spacing, n = (4
 69              if unit.base_shape in (TileShape.RECTANGLE, )
 70              else (6
 71                if unit.base_shape in (TileShape.HEXAGON, )
 72                else 3)))
 73  unit.prototile = gpd.GeoDataFrame(
 74    geometry = gpd.GeoSeries([tile]), crs = unit.crs)
 75  unit.setup_vectors()
 76  return
 77
 78
 79def setup_cairo(unit:TileUnit) -> None:
 80  """Sets up the Cairo tiling. King of tilings. All hail the Cairo tiling.
 81  This code shows how a 'handcoded' set of geometries can be applied.
 82
 83  Note that it is advisable to avoid intersection and union operations where
 84  possible, as it often yields floating point mismatches that can be hard
 85  to repair! (This tiling can be relatively conveniently generated by
 86  dissecting a square in 4 quarters at a 30 degree angle to the sides and
 87  then reflecting and rotating copies and joing them back together. But
 88  floating point issues make that very messy indeed. Much better to make
 89  the geometries 'pure'.)
 90
 91  Args:
 92    unit (TileUnit):  the TileUnit to setup.
 93  """
 94  _setup_base_tile(unit, TileShape.RECTANGLE)  # a square
 95  d = unit.spacing
 96  x = d / 2 / (np.cos(np.radians(15)) + np.cos(np.radians(75)))
 97  # the following is just the geometry, it is what it is...
 98  # points are (more or less)
 99  #
100  #         3
101  #     2
102  #            4
103  #     1    0
104  #
105  # then rotate -15 and make 4 copies at 90 degree rotations
106  p1 = geom.Polygon([(x, 0), (0, 0), (0, x),
107             (x * np.sqrt(3) / 2, x + x / 2),
108             (x * (1 + np.sqrt(3)) / 2, x * (3 - np.sqrt(3)) / 2)])
109  p1 = affine.rotate(p1, -15, (0, 0))
110  p2 = affine.rotate(p1, 90, (0, 0))
111  p3 = affine.rotate(p1, 180, (0, 0))
112  p4 = affine.rotate(p1, 270, (0, 0))
113
114  # now move them so they are arranged as a hexagon centered on the tile
115  p1 = affine.translate(p1, -unit.spacing / 2, 0)
116  p2 = affine.translate(p2, unit.spacing / 2, 0)
117  p3 = affine.translate(p3, unit.spacing / 2, 0)
118  p4 = affine.translate(p4, -unit.spacing / 2, 0)
119
120  unit.tiles = gpd.GeoDataFrame(
121    data = {"tile_id": list("abcd")}, crs = unit.crs,
122    geometry = gpd.GeoSeries([p1, p2, p3, p4]))
123  unit.setup_regularised_prototile_from_tiles()
124
125
126def setup_hex_slice(unit:TileUnit) -> None:
127  """Tilings from radial slices of a hexagon into 2, 3, 4, 6 or 12 slices.
128
129  The supplied unit should have offset and n set.
130
131  self.offset == 1 starts at midpoints, 0 at hexagon corners
132  self.n is the number of slices and should be 2, 3, 4, 6 or 12.
133
134  Again, construction avoids intersection operations where possible.
135
136  Args:
137    unit (TileUnit):  the TileUnit to setup.
138  """
139  _setup_base_tile(unit, TileShape.HEXAGON)
140  hexagon = tiling_utils.get_regular_polygon(unit.spacing, 6)
141  # note that shapely coords includes the first point at beginning
142  # and end - very convenient!
143  v = list(hexagon.exterior.coords)
144  # midpoints
145  m = [((p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2)
146          for p1, p2 in zip(v[:-1], v[1:])]
147  # chain into a list of corner, midpoint, corner, midpoint..., i.e.
148  #
149  #           6  5  4
150  #          7       3
151  #         8         2
152  #          9       1
153  #           10 11 0
154  #
155  # then depending on number of slices and offset pick out points.
156  # Do this explicitly not by rotation to avoid gaps.
157  p = list(itertools.chain(*zip(v[:-1], m)))
158  if unit.n == 2:
159    slices = (
160      [geom.Polygon([p[0], p[2], p[4], p[6]]),
161       geom.Polygon([p[6], p[8], p[10], p[0]])]
162      if unit.offset == 0 else
163      [geom.Polygon([p[1], p[2], p[4], p[6], p[7]]),
164       geom.Polygon([p[7], p[8], p[10], p[0], p[1]])])
165  elif unit.n == 3:
166    slices = (
167      [geom.Polygon([p[0], p[2], p[4], (0, 0)]),
168       geom.Polygon([p[4], p[6], p[8], (0, 0)]),
169       geom.Polygon([p[8], p[10], p[0], (0, 0)])]
170      if unit.offset == 0 else
171      [geom.Polygon([p[1], p[2], p[4], p[5], (0, 0)]),
172       geom.Polygon([p[5], p[6], p[8], p[9], (0, 0)]),
173       geom.Polygon([p[9], p[10], p[0], p[1], (0, 0)])])
174  elif unit.n == 4:
175    slices = (
176      [geom.Polygon([p[0], p[2], p[3], (0, 0)]),
177       geom.Polygon([p[3], p[4], p[6], (0, 0)]),
178       geom.Polygon([p[6], p[8], p[9], (0, 0)]),
179       geom.Polygon([p[9], p[10], p[0], (0, 0)])]
180      if unit.offset == 0 else
181      [geom.Polygon([p[1], p[2], p[4], (0, 0)]),
182       geom.Polygon([p[4], p[6], p[7], (0, 0)]),
183       geom.Polygon([p[7], p[8], p[10], (0, 0)]),
184       geom.Polygon([p[10], p[0], p[1], (0, 0)])])
185  elif unit.n == 6:
186    slices = (
187      [geom.Polygon([p[0], p[2], (0, 0)]),
188       geom.Polygon([p[2], p[4], (0, 0)]),
189       geom.Polygon([p[4], p[6], (0, 0)]),
190       geom.Polygon([p[6], p[8], (0, 0)]),
191       geom.Polygon([p[8], p[10], (0, 0)]),
192       geom.Polygon([p[10], p[0], (0, 0)])]
193      if unit.offset == 0 else
194      [geom.Polygon([p[1], p[2], p[3], (0, 0)]),
195       geom.Polygon([p[3], p[4], p[5], (0, 0)]),
196       geom.Polygon([p[5], p[6], p[7], (0, 0)]),
197       geom.Polygon([p[7], p[8], p[9], (0, 0)]),
198       geom.Polygon([p[9], p[10], p[11], (0, 0)]),
199       geom.Polygon([p[11], p[0], p[1], (0, 0)])])
200  elif unit.n == 12:
201    if unit.offset == 0:
202      ids = [i for i in range(12)] + [0]
203      slices = [geom.Polygon([p[i], p[j], (0, 0)])
204                for i, j in zip(ids[:-1], ids[1:])]
205    else:
206      x = (np.sin(np.pi/6) - np.sin(np.pi/12)) / 6
207      base = [x, 1/6 - x]
208      steps = base
209      for i in range(1, 6):
210        steps = steps + [x + i/6 for x in base]
211      steps = [steps[-1] - 1] + steps
212      slices = [tiling_utils.get_polygon_sector(hexagon, p1, p2)
213                for p1, p2 in zip(steps[:-1], steps[1:])]
214
215  unit.tiles = gpd.GeoDataFrame(
216    data = {"tile_id": list(string.ascii_letters)[:unit.n]},
217    crs = unit.crs,
218    geometry = gpd.GeoSeries(slices))
219  unit.regularised_prototile = copy.deepcopy(unit.prototile)
220
221
222def setup_hex_dissection(unit:TileUnit) -> None:
223  """Tilings from dissection of a hexagon into parts.
224
225  The supplied unit should have offset and n set.
226
227  self.offset == 1 starts at midpoints, 0 at hexagon corners
228  self.n is the number of slices and should be 2, 3, 4, 6 or 12.
229
230  Args:
231    unit (TileUnit):  the TileUnit to setup.
232  """
233  _setup_base_tile(unit, TileShape.HEXAGON)
234  if unit.n == 4:
235    parts = get_4_parts_of_hexagon(unit)
236  elif unit.n == 7:
237    parts = get_7_parts_of_hexagon(unit)
238  elif unit.n == 9:
239    parts = get_9_parts_of_hexagon(unit)
240  unit.tiles = gpd.GeoDataFrame(
241    data = {"tile_id": list(string.ascii_letters)[:unit.n]},
242    crs = unit.crs,
243    geometry = gpd.GeoSeries(parts))
244  unit.regularised_prototile = copy.deepcopy(unit.prototile)
245
246
247def get_4_parts_of_hexagon(unit: TileUnit) -> list[geom.Polygon]:
248  outer_h = tiling_utils.get_regular_polygon(unit.spacing, 6)
249  inner_h = affine.scale(outer_h, 1/np.sqrt(3), 1/np.sqrt(3))
250  if unit.offset == 1:
251    inner_h = affine.rotate(inner_h, 30, (0, 0))
252  o_hx = tiling_utils.get_corners(outer_h)
253  i_hx = tiling_utils.get_corners(inner_h)
254  if unit.offset == 1:
255    o = []
256    for p1, p2 in zip(o_hx[:-1], o_hx[1:]):
257      o.extend([p1, geom.Point([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2])])
258    return [
259      inner_h,
260      geom.Polygon([i_hx[2], i_hx[1], i_hx[0], o[11], o[0], o[2], o[3]]),
261      geom.Polygon([i_hx[4], i_hx[3], i_hx[2], o[3], o[4], o[6], o[7]]),
262      geom.Polygon([i_hx[0], i_hx[5], i_hx[4], o[7], o[8], o[10], o[11]])
263    ]
264  else:
265    return [
266      inner_h,
267      geom.Polygon([i_hx[2], i_hx[1], i_hx[0], o_hx[0], o_hx[1], o_hx[2]]),
268      geom.Polygon([i_hx[4], i_hx[3], i_hx[2], o_hx[2], o_hx[3], o_hx[4]]),
269      geom.Polygon([i_hx[0], i_hx[5], i_hx[4], o_hx[4], o_hx[5], o_hx[0]])
270    ]
271
272
273def get_7_parts_of_hexagon(unit: TileUnit) -> list[geom.Polygon]:
274  outer_h = tiling_utils.get_regular_polygon(unit.spacing, 6)
275  inner_h = affine.scale(outer_h, 1/np.sqrt(7), 1/np.sqrt(7))
276  if unit.offset == 1:
277    inner_h = affine.rotate(inner_h, 30, (0, 0))
278  outer = tiling_utils.get_corners(outer_h)
279  inner = tiling_utils.get_corners(inner_h)
280  if unit.offset == 1:
281    o = []
282    for p1, p2 in zip(outer[:-1], outer[1:]):
283      o.extend([p1, geom.Point([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2])])
284    i = []
285    for p1, p2 in zip(inner[:-1], inner[1:]):
286      i.extend([p1, geom.Point([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2])])
287    return [
288      inner_h,
289      geom.Polygon([i[2], i[0], o[11], o[0], o[1]]),
290      geom.Polygon([i[4], i[2]] + o[1:4]),
291      geom.Polygon([i[6], i[4]] + o[3:6]),
292      geom.Polygon([i[8], i[6]] + o[5:8]),
293      geom.Polygon([i[10], i[8]] + o[7:10]),
294      geom.Polygon([i[0], i[10]] + o[9:])
295    ]
296  else:
297    return [
298      inner_h,
299      geom.Polygon([inner[1], inner[0], outer[0], outer[1]]),
300      geom.Polygon([inner[2], inner[1], outer[1], outer[2]]),
301      geom.Polygon([inner[3], inner[2], outer[2], outer[3]]),
302      geom.Polygon([inner[4], inner[3], outer[3], outer[4]]),
303      geom.Polygon([inner[5], inner[4], outer[4], outer[5]]),
304      geom.Polygon([inner[0], inner[5], outer[5], outer[0]])
305    ]
306
307
308def get_9_parts_of_hexagon(unit: TileUnit) -> list[geom.Polygon]:
309  c = geom.Point(0, 0)
310  outer_h = tiling_utils.get_regular_polygon(unit.spacing, 6)
311  inner_h = affine.scale(outer_h, 1/np.sqrt(3), 1/np.sqrt(3))
312  if unit.offset == 1:
313    inner_h = affine.rotate(inner_h, 30, (0, 0))
314  outer = tiling_utils.get_corners(outer_h)
315  inner = tiling_utils.get_corners(inner_h)
316  if unit.offset == 1:
317    o = []
318    for p1, p2 in zip(outer[:-1], outer[1:]):
319      o.extend([p1, geom.Point([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2])])
320    i = []
321    for p1, p2 in zip(inner[:-1], inner[1:]):
322      i.extend([p1, geom.Point([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2])])
323    return [
324      geom.Polygon([c, i[1], i[2], i[4], i[5]]),
325      geom.Polygon([c, i[5], i[6], i[8], i[9]]),
326      geom.Polygon([c, i[9], i[10], i[0], i[1]]),
327      geom.Polygon([o[0], o[1], i[2], i[0], o[11]]),
328      geom.Polygon([o[2], o[3], i[4], i[2], o[1]]),
329      geom.Polygon([o[4], o[5], i[6], i[4], o[3]]),
330      geom.Polygon([o[6], o[7], i[8], i[6], o[5]]),
331      geom.Polygon([o[8], o[9], i[10], i[8], o[7]]),
332      geom.Polygon([o[10], o[11], i[0], i[10], o[9]])
333    ]
334  else:
335    return [
336      geom.Polygon([c, inner[0], inner[1], inner[2]]),
337      geom.Polygon([c, inner[2], inner[3], inner[4]]),
338      geom.Polygon([c, inner[4], inner[5], inner[0]]),
339      geom.Polygon([inner[1], inner[0], outer[0], outer[1]]),
340      geom.Polygon([inner[2], inner[1], outer[1], outer[2]]),
341      geom.Polygon([inner[3], inner[2], outer[2], outer[3]]),
342      geom.Polygon([inner[4], inner[3], outer[3], outer[4]]),
343      geom.Polygon([inner[5], inner[4], outer[4], outer[5]]),
344      geom.Polygon([inner[0], inner[5], outer[5], outer[0]])
345    ]
346
347
348def setup_laves(unit:TileUnit) -> None:
349  """The Laves tilings. See https://en.wikipedia.org/wiki/List_of_Euclidean_uniform_tilings#Laves_tilings.
350
351  These are all isohedral, but mostly not regular polygons. We
352  prioritise them over the Archimedean tilings because being
353  isohedral all tiles are the same size. Several are hex
354  dissections and setup is delegated accordingly.
355
356  Args:
357    unit (TileUnit):  the TileUnit to setup.
358  """
359  if unit.code == "3.3.3.3.3.3":
360    # this is the regular hexagons
361    _setup_base_tile(unit, TileShape.HEXAGON)
362    _setup_none_tile(unit)
363    return
364  if unit.code == "3.3.3.3.6":
365    # this one needs its own code
366    _setup_laves_33336(unit)
367    return
368  elif unit.code == "3.3.3.4.4":
369    print(f"The code [{unit.code}] is unsupported.")
370  elif unit.code == "3.3.4.3.4":
371    # king of tilings!
372    setup_cairo(unit)
373    return
374  elif unit.code == "3.4.6.4":
375    # the hex 6-dissection
376    unit.n = 6
377    unit.offset = 1
378    setup_hex_slice(unit)
379    return
380  elif unit.code == "3.6.3.6":
381    # hex 3-dissection (also a cube weave!)
382    unit.n = 3
383    unit.offset = 0
384    setup_hex_slice(unit)
385    return
386  elif unit.code == "3.12.12":
387    # again this one needs its own
388    _setup_laves_31212(unit)
389    return
390  elif unit.code == "4.4.4.4":
391    # square grid
392    _setup_base_tile(unit, TileShape.RECTANGLE)
393    _setup_none_tile(unit)
394    return
395  elif unit.code == "4.6.12":
396    # hex 12-dissection
397    unit.n = 12
398    unit.offset = 0
399    setup_hex_slice(unit)
400    return
401  elif unit.code == "4.8.8":
402    # this one needs its own (a 4-dissection of the square)
403    # perhaps to be added as a category later...
404    _setup_laves_488(unit)
405    return
406  elif unit.code == "6.6.6":
407    # triangles
408    _setup_base_tile(unit, TileShape.TRIANGLE)
409    _setup_none_tile(unit)
410  else:
411    print(f"[{unit.code}] is not a valid Laves code.")
412
413  unit.tiling_type = None
414  _setup_none_tile(unit)
415  return
416
417
418def _setup_laves_33336(unit:TileUnit) -> None:
419  """Sets up Laves [3.3.3.3.6] which is like a 6 petal flower.
420
421  Similar to the H3 7 hexagon group with the central hex removed and each
422  hex 'taking' a 1/6 share of the central hex.
423
424  Args:
425    unit (TileUnit):  the TileUnit to setup.
426  """
427  _setup_base_tile(unit, TileShape.HEXAGON)
428  offset_a = np.degrees(np.arctan(1 / 3 / np.sqrt(3)))
429  sf = 1 / np.sqrt(7)
430  tile = tiling_utils.get_regular_polygon(unit.spacing, 6)
431  hexagon = affine.scale(tile, sf, sf)
432  # translate it up by its own height
433  hexagon = affine.translate(hexagon, 0, hexagon.bounds[3] - hexagon.bounds[1])
434  hex_p = [p for p in hexagon.exterior.coords]
435  # now replace first and last points by (0, 0)
436  # (note shapely doesn't require closure of the polygon)
437  petal = geom.Polygon([(0, 0)] + hex_p[1:5])
438  petals = [
439    tiling_utils.gridify(affine.rotate(petal, a + offset_a, origin = (0, 0)))
440    for a in range(30, 360, 60)]
441  unit.tiles = gpd.GeoDataFrame(
442    data = {"tile_id": list("abcdef")},
443    crs = unit.crs,
444    geometry = gpd.GeoSeries(petals))
445  unit.setup_regularised_prototile_from_tiles()
446
447
448def _setup_laves_488(unit:TileUnit) -> None:
449  """The 4-dissection of the square by its diagonals.
450
451  Args:
452    unit (TileUnit):  the TileUnit to setup.
453  """
454  _setup_base_tile(unit, TileShape.RECTANGLE)
455  tile = tiling_utils.get_regular_polygon(unit.spacing, 4)
456  pts = [p for p in tile.exterior.coords]
457  tris = [geom.Polygon([pts[i], pts[i+1], (0, 0)]) for i in range(4)]
458  unit.tiles = gpd.GeoDataFrame(
459    data = {"tile_id": list("abcd")},
460    crs = unit.crs,
461    geometry = gpd.GeoSeries(tris))
462  unit.setup_regularised_prototile_from_tiles()
463
464
465def _setup_laves_31212(unit:TileUnit) -> None:
466  """This is also a hexagon dissection... like 3.6.3.6 with each rhombus
467  sliced in half along its long diagonal.
468
469  Args:
470    unit (TileUnit):  the TileUnit to setup.
471  """
472  _setup_base_tile(unit, TileShape.HEXAGON)
473  hexagon = tiling_utils.get_regular_polygon(unit.spacing, 6)
474  pts = [p for p in hexagon.exterior.coords]
475  tri1 = geom.Polygon([pts[0], pts[2], [0, 0]])
476  tri2 = geom.Polygon([pts[0], pts[1], pts[2]])
477  tris1 = [affine.rotate(tri1, a, origin = (0, 0))
478       for a in range(0, 360, 120)]
479  tris2 = [affine.rotate(tri2, a, origin = (0, 0))
480       for a in range(0, 360, 120)]
481  # reorder so the 'inner' and 'outer' triangles are labelled alternately
482  tris = itertools.chain(*zip(tris1, tris2))
483  unit.tiles = gpd.GeoDataFrame(
484    data = {"tile_id": list("abcdef")}, crs = unit.crs,
485    geometry = gpd.GeoSeries(tris)
486  )
487  unit.regularised_prototile = copy.deepcopy(unit.prototile)
488
489
490def setup_archimedean(unit:TileUnit) -> None:
491  """The Archimedean 'regular tilings. See https://en.wikipedia.org/wiki/List_of_Euclidean_uniform_tilings#Convex_uniform_tilings_of_the_Euclidean_plane
492
493  Many of these are most easily constructed as duals of the Laves tilings.
494
495  Some are not yet implemented:
496
497  (3.3.3.4.4) is kind of weird (stripes of triangles and squares) so can't be
498  bothered with it. Perhaps as a 5-variable option we'll get to it in time.
499
500  Args:
501    unit (TileUnit):  the TileUnit to setup.
502  """
503  if unit.code == "3.3.3.3.3.3":
504    _setup_base_tile(unit, TileShape.TRIANGLE)
505    _setup_none_tile(unit)
506    return
507  if unit.code == "3.3.3.3.6":
508    setup_laves(unit)
509    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
510    unit.setup_regularised_prototile_from_tiles()
511    return
512  elif unit.code == "3.3.3.4.4":
513    print(f"The code [{unit.code}] is unsupported.")
514  elif unit.code == "3.3.4.3.4":
515    # this is an attractive 6-colourable triangles and squares tiling
516    setup_laves(unit)
517    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
518    unit.setup_regularised_prototile_from_tiles()
519    return
520  elif unit.code == "3.4.6.4":
521    setup_laves(unit)
522    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
523    unit.setup_regularised_prototile_from_tiles()
524    return
525  elif unit.code == "3.6.3.6":
526    setup_laves(unit)
527    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
528    unit.setup_regularised_prototile_from_tiles()
529    return
530  elif unit.code == "3.12.12":
531    # nice! we can make dodecagons without having to think too hard
532    # simply use the dual code. (Although really... it probably
533    # would've been easier to make the dodecagon... other than
534    # calculating the scale relative to the hexagon base tile!)
535    setup_laves(unit)
536    unit.setup_vectors()
537    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
538    unit.setup_regularised_prototile_from_tiles()
539    return
540  elif unit.code == "4.4.4.4":
541    _setup_base_tile(unit, TileShape.RECTANGLE)
542    _setup_none_tile(unit)
543    return
544  elif unit.code == "4.6.12":
545    # more dodecagons for free!
546    setup_laves(unit)
547    unit.setup_vectors()
548    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
549    unit.setup_regularised_prototile_from_tiles()
550    return
551  elif unit.code == "4.8.8":
552    # this is the octagon and square tiling
553    setup_laves(unit)
554    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
555    unit.setup_regularised_prototile_from_tiles()
556    return
557  elif unit.code == "6.6.6":
558    _setup_base_tile(unit, TileShape.HEXAGON)
559    _setup_none_tile(unit)
560    return
561  else:
562    print(f"[{unit.code}] is not a valid Laves code.")
563
564  unit.tiling_type = None
565  _setup_none_tile(unit)
566  return
567
568
569def _setup_archimedean_3464(unit:TileUnit) -> None:
570  """The dual of Laves 3.4.6.4 is not accurately rendered
571  by our code, so we do this one by hand.
572
573  Args:
574    unit (TileUnit):  the TileUnit to setup.
575  """
576  _setup_base_tile(unit, TileShape.HEXAGON)
577  sf = np.sqrt(3) / (1 + np.sqrt(3))
578  hexagon = tiling_utils.get_regular_polygon(unit.spacing * sf, 6)
579  corners = [p for p in hexagon.exterior.coords]
580  p1 = corners[1]
581  p2 = corners[0]
582  #   HEX   p1
583  #        /    p4
584  #    ---p2   /
585  #           p3
586  dx, dy = p2[0] - p1[0], p2[1] - p1[1]
587  p3 = (p2[0] - dy, p2[1] + dx)
588  p4 = (p3[0] - dx, p3[1] - dy)
589  square1 = geom.Polygon([p1, p2, p3, p4])
590  square2 = affine.rotate(square1, 60, (0, 0))
591  square3 = affine.rotate(square2, 60, (0, 0))
592  p5 = [pt for pt in square2.exterior.coords][2]
593  tri1 = geom.Polygon([p1, p4, p5])
594  tri2 = affine.rotate(tri1, 60, (0, 0))
595
596  unit.tiles = gpd.GeoDataFrame(
597    data = {"tile_id": list("abcdef")},
598    crs = unit.crs,
599    geometry = gpd.GeoSeries([hexagon, square1, square2, square3, tri1, tri2]))
600  unit.setup_regularised_prototile_from_tiles()
601
602
603def setup_hex_colouring(unit:TileUnit) -> None:
604  """3, 4, and 7 colourings of a regular array of hexagons.
605
606  Args:
607    unit (TileUnit):  the TileUnit to setup.
608  """
609  hexagon = tiling_utils.get_regular_polygon(unit.spacing / np.sqrt(unit.n), 6)
610  if unit.n == 3:
611    # Point up hex at '*' displaced to 3 positions:
612    #      2
613    #      *
614    #    3   1
615    _setup_base_tile(unit, TileShape.HEXAGON)
616    hexagon = affine.rotate(hexagon, 30, origin = (0, 0))
617    # Copy and translate to alternate corners
618    corners = [p for i, p in enumerate(hexagon.exterior.coords)
619           if i in (0, 2, 4)]
620    hexes = [affine.translate(hexagon, p[0], p[1]) for p in corners]
621  elif unit.n == 4:
622    # Point up hex at '*' displaced to 4 positions:
623    #      2
624    #     3*1
625    #      4
626    _setup_base_tile(unit, TileShape.DIAMOND)
627    hexagon = affine.rotate(hexagon, 30, origin = (0, 0))
628    hex1 = affine.translate(hexagon, unit.spacing / 4, 0)
629    hex2 = affine.translate(hexagon, 0, unit.spacing * np.sqrt(3) / 4)
630    hex3 = affine.translate(hexagon, -unit.spacing / 4, 0)
631    hex4 = affine.translate(hexagon, 0, -unit.spacing * np.sqrt(3) / 4)
632    hexes = [hex1, hex2, hex3, hex4]
633  elif unit.n == 7:  # the 'H3' tile
634    # Make a hexagon and displace in the direction of its
635    # own 6 corners, scaled as needed
636    _setup_base_tile(unit, TileShape.HEXAGON)
637    rotation = np.degrees(np.arctan(1 / 3 / np.sqrt(3)))
638    corners = [p for p in hexagon.exterior.coords][:-1]
639    hexagon = affine.rotate(hexagon, 30)
640    hexes = [hexagon] + [affine.translate(
641      hexagon, x * np.sqrt(3), y * np.sqrt(3)) for x, y in corners]
642    hexes = [affine.rotate(h, rotation, origin = (0, 0))
643             for h in hexes]
644  else:
645    _setup_base_tile(unit, TileShape.HEXAGON)
646    _setup_none_tile(unit)
647    return
648
649  unit.tiles = gpd.GeoDataFrame(
650    data = {"tile_id": list(string.ascii_letters)[:unit.n]},
651    crs = unit.crs,
652    geometry = gpd.GeoSeries(hexes))
653  unit.setup_regularised_prototile_from_tiles()
654
655
656def setup_square_colouring(unit:TileUnit) -> None:
657  """Colourings of a regular array of squares. Only supports n = 5 at present
658  but we need a n=5 option
659
660  Args:
661    unit (TileUnit):  the TileUnit to setup.
662  """
663  sq = tiling_utils.get_regular_polygon(unit.spacing, 4)
664  if unit.n == 5:
665    _setup_base_tile(unit, TileShape.RECTANGLE)
666
667    # Copy and translate square
668    tr = [(0, 0), (unit.spacing, 0), (0, unit.spacing),
669        (-unit.spacing, 0), (0, -unit.spacing)]
670    squares = [affine.translate(sq, v[0], v[1]) for v in tr]
671    squares = [affine.scale(sq, 1 / np.sqrt(5), 1 / np.sqrt(5),
672                origin = (0, 0)) for sq in squares]
673    rotation = np.degrees(np.arctan2(1, 2))
674    squares = [affine.rotate(sq, rotation, origin = (0, 0))
675           for sq in squares]
676  else:
677    _setup_base_tile(unit, TileShape.RECTANGLE)
678    _setup_none_tile(unit)
679    return
680
681  unit.tiles = gpd.GeoDataFrame(
682    data = {"tile_id": list(string.ascii_letters)[:unit.n]},
683    crs = unit.crs,
684    geometry = gpd.GeoSeries(squares))
685  unit.setup_regularised_prototile_from_tiles()
def setup_cairo(unit: weavingspace.tile_unit.TileUnit) -> None:
 80def setup_cairo(unit:TileUnit) -> None:
 81  """Sets up the Cairo tiling. King of tilings. All hail the Cairo tiling.
 82  This code shows how a 'handcoded' set of geometries can be applied.
 83
 84  Note that it is advisable to avoid intersection and union operations where
 85  possible, as it often yields floating point mismatches that can be hard
 86  to repair! (This tiling can be relatively conveniently generated by
 87  dissecting a square in 4 quarters at a 30 degree angle to the sides and
 88  then reflecting and rotating copies and joing them back together. But
 89  floating point issues make that very messy indeed. Much better to make
 90  the geometries 'pure'.)
 91
 92  Args:
 93    unit (TileUnit):  the TileUnit to setup.
 94  """
 95  _setup_base_tile(unit, TileShape.RECTANGLE)  # a square
 96  d = unit.spacing
 97  x = d / 2 / (np.cos(np.radians(15)) + np.cos(np.radians(75)))
 98  # the following is just the geometry, it is what it is...
 99  # points are (more or less)
100  #
101  #         3
102  #     2
103  #            4
104  #     1    0
105  #
106  # then rotate -15 and make 4 copies at 90 degree rotations
107  p1 = geom.Polygon([(x, 0), (0, 0), (0, x),
108             (x * np.sqrt(3) / 2, x + x / 2),
109             (x * (1 + np.sqrt(3)) / 2, x * (3 - np.sqrt(3)) / 2)])
110  p1 = affine.rotate(p1, -15, (0, 0))
111  p2 = affine.rotate(p1, 90, (0, 0))
112  p3 = affine.rotate(p1, 180, (0, 0))
113  p4 = affine.rotate(p1, 270, (0, 0))
114
115  # now move them so they are arranged as a hexagon centered on the tile
116  p1 = affine.translate(p1, -unit.spacing / 2, 0)
117  p2 = affine.translate(p2, unit.spacing / 2, 0)
118  p3 = affine.translate(p3, unit.spacing / 2, 0)
119  p4 = affine.translate(p4, -unit.spacing / 2, 0)
120
121  unit.tiles = gpd.GeoDataFrame(
122    data = {"tile_id": list("abcd")}, crs = unit.crs,
123    geometry = gpd.GeoSeries([p1, p2, p3, p4]))
124  unit.setup_regularised_prototile_from_tiles()

Sets up the Cairo tiling. King of tilings. All hail the Cairo tiling. This code shows how a 'handcoded' set of geometries can be applied.

Note that it is advisable to avoid intersection and union operations where possible, as it often yields floating point mismatches that can be hard to repair! (This tiling can be relatively conveniently generated by dissecting a square in 4 quarters at a 30 degree angle to the sides and then reflecting and rotating copies and joing them back together. But floating point issues make that very messy indeed. Much better to make the geometries 'pure'.)

Args: unit (TileUnit): the TileUnit to setup.

def setup_hex_slice(unit: weavingspace.tile_unit.TileUnit) -> None:
127def setup_hex_slice(unit:TileUnit) -> None:
128  """Tilings from radial slices of a hexagon into 2, 3, 4, 6 or 12 slices.
129
130  The supplied unit should have offset and n set.
131
132  self.offset == 1 starts at midpoints, 0 at hexagon corners
133  self.n is the number of slices and should be 2, 3, 4, 6 or 12.
134
135  Again, construction avoids intersection operations where possible.
136
137  Args:
138    unit (TileUnit):  the TileUnit to setup.
139  """
140  _setup_base_tile(unit, TileShape.HEXAGON)
141  hexagon = tiling_utils.get_regular_polygon(unit.spacing, 6)
142  # note that shapely coords includes the first point at beginning
143  # and end - very convenient!
144  v = list(hexagon.exterior.coords)
145  # midpoints
146  m = [((p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2)
147          for p1, p2 in zip(v[:-1], v[1:])]
148  # chain into a list of corner, midpoint, corner, midpoint..., i.e.
149  #
150  #           6  5  4
151  #          7       3
152  #         8         2
153  #          9       1
154  #           10 11 0
155  #
156  # then depending on number of slices and offset pick out points.
157  # Do this explicitly not by rotation to avoid gaps.
158  p = list(itertools.chain(*zip(v[:-1], m)))
159  if unit.n == 2:
160    slices = (
161      [geom.Polygon([p[0], p[2], p[4], p[6]]),
162       geom.Polygon([p[6], p[8], p[10], p[0]])]
163      if unit.offset == 0 else
164      [geom.Polygon([p[1], p[2], p[4], p[6], p[7]]),
165       geom.Polygon([p[7], p[8], p[10], p[0], p[1]])])
166  elif unit.n == 3:
167    slices = (
168      [geom.Polygon([p[0], p[2], p[4], (0, 0)]),
169       geom.Polygon([p[4], p[6], p[8], (0, 0)]),
170       geom.Polygon([p[8], p[10], p[0], (0, 0)])]
171      if unit.offset == 0 else
172      [geom.Polygon([p[1], p[2], p[4], p[5], (0, 0)]),
173       geom.Polygon([p[5], p[6], p[8], p[9], (0, 0)]),
174       geom.Polygon([p[9], p[10], p[0], p[1], (0, 0)])])
175  elif unit.n == 4:
176    slices = (
177      [geom.Polygon([p[0], p[2], p[3], (0, 0)]),
178       geom.Polygon([p[3], p[4], p[6], (0, 0)]),
179       geom.Polygon([p[6], p[8], p[9], (0, 0)]),
180       geom.Polygon([p[9], p[10], p[0], (0, 0)])]
181      if unit.offset == 0 else
182      [geom.Polygon([p[1], p[2], p[4], (0, 0)]),
183       geom.Polygon([p[4], p[6], p[7], (0, 0)]),
184       geom.Polygon([p[7], p[8], p[10], (0, 0)]),
185       geom.Polygon([p[10], p[0], p[1], (0, 0)])])
186  elif unit.n == 6:
187    slices = (
188      [geom.Polygon([p[0], p[2], (0, 0)]),
189       geom.Polygon([p[2], p[4], (0, 0)]),
190       geom.Polygon([p[4], p[6], (0, 0)]),
191       geom.Polygon([p[6], p[8], (0, 0)]),
192       geom.Polygon([p[8], p[10], (0, 0)]),
193       geom.Polygon([p[10], p[0], (0, 0)])]
194      if unit.offset == 0 else
195      [geom.Polygon([p[1], p[2], p[3], (0, 0)]),
196       geom.Polygon([p[3], p[4], p[5], (0, 0)]),
197       geom.Polygon([p[5], p[6], p[7], (0, 0)]),
198       geom.Polygon([p[7], p[8], p[9], (0, 0)]),
199       geom.Polygon([p[9], p[10], p[11], (0, 0)]),
200       geom.Polygon([p[11], p[0], p[1], (0, 0)])])
201  elif unit.n == 12:
202    if unit.offset == 0:
203      ids = [i for i in range(12)] + [0]
204      slices = [geom.Polygon([p[i], p[j], (0, 0)])
205                for i, j in zip(ids[:-1], ids[1:])]
206    else:
207      x = (np.sin(np.pi/6) - np.sin(np.pi/12)) / 6
208      base = [x, 1/6 - x]
209      steps = base
210      for i in range(1, 6):
211        steps = steps + [x + i/6 for x in base]
212      steps = [steps[-1] - 1] + steps
213      slices = [tiling_utils.get_polygon_sector(hexagon, p1, p2)
214                for p1, p2 in zip(steps[:-1], steps[1:])]
215
216  unit.tiles = gpd.GeoDataFrame(
217    data = {"tile_id": list(string.ascii_letters)[:unit.n]},
218    crs = unit.crs,
219    geometry = gpd.GeoSeries(slices))
220  unit.regularised_prototile = copy.deepcopy(unit.prototile)

Tilings from radial slices of a hexagon into 2, 3, 4, 6 or 12 slices.

The supplied unit should have offset and n set.

self.offset == 1 starts at midpoints, 0 at hexagon corners self.n is the number of slices and should be 2, 3, 4, 6 or 12.

Again, construction avoids intersection operations where possible.

Args: unit (TileUnit): the TileUnit to setup.

def setup_hex_dissection(unit: weavingspace.tile_unit.TileUnit) -> None:
223def setup_hex_dissection(unit:TileUnit) -> None:
224  """Tilings from dissection of a hexagon into parts.
225
226  The supplied unit should have offset and n set.
227
228  self.offset == 1 starts at midpoints, 0 at hexagon corners
229  self.n is the number of slices and should be 2, 3, 4, 6 or 12.
230
231  Args:
232    unit (TileUnit):  the TileUnit to setup.
233  """
234  _setup_base_tile(unit, TileShape.HEXAGON)
235  if unit.n == 4:
236    parts = get_4_parts_of_hexagon(unit)
237  elif unit.n == 7:
238    parts = get_7_parts_of_hexagon(unit)
239  elif unit.n == 9:
240    parts = get_9_parts_of_hexagon(unit)
241  unit.tiles = gpd.GeoDataFrame(
242    data = {"tile_id": list(string.ascii_letters)[:unit.n]},
243    crs = unit.crs,
244    geometry = gpd.GeoSeries(parts))
245  unit.regularised_prototile = copy.deepcopy(unit.prototile)

Tilings from dissection of a hexagon into parts.

The supplied unit should have offset and n set.

self.offset == 1 starts at midpoints, 0 at hexagon corners self.n is the number of slices and should be 2, 3, 4, 6 or 12.

Args: unit (TileUnit): the TileUnit to setup.

def get_4_parts_of_hexagon( unit: weavingspace.tile_unit.TileUnit) -> list[shapely.geometry.polygon.Polygon]:
248def get_4_parts_of_hexagon(unit: TileUnit) -> list[geom.Polygon]:
249  outer_h = tiling_utils.get_regular_polygon(unit.spacing, 6)
250  inner_h = affine.scale(outer_h, 1/np.sqrt(3), 1/np.sqrt(3))
251  if unit.offset == 1:
252    inner_h = affine.rotate(inner_h, 30, (0, 0))
253  o_hx = tiling_utils.get_corners(outer_h)
254  i_hx = tiling_utils.get_corners(inner_h)
255  if unit.offset == 1:
256    o = []
257    for p1, p2 in zip(o_hx[:-1], o_hx[1:]):
258      o.extend([p1, geom.Point([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2])])
259    return [
260      inner_h,
261      geom.Polygon([i_hx[2], i_hx[1], i_hx[0], o[11], o[0], o[2], o[3]]),
262      geom.Polygon([i_hx[4], i_hx[3], i_hx[2], o[3], o[4], o[6], o[7]]),
263      geom.Polygon([i_hx[0], i_hx[5], i_hx[4], o[7], o[8], o[10], o[11]])
264    ]
265  else:
266    return [
267      inner_h,
268      geom.Polygon([i_hx[2], i_hx[1], i_hx[0], o_hx[0], o_hx[1], o_hx[2]]),
269      geom.Polygon([i_hx[4], i_hx[3], i_hx[2], o_hx[2], o_hx[3], o_hx[4]]),
270      geom.Polygon([i_hx[0], i_hx[5], i_hx[4], o_hx[4], o_hx[5], o_hx[0]])
271    ]
def get_7_parts_of_hexagon( unit: weavingspace.tile_unit.TileUnit) -> list[shapely.geometry.polygon.Polygon]:
274def get_7_parts_of_hexagon(unit: TileUnit) -> list[geom.Polygon]:
275  outer_h = tiling_utils.get_regular_polygon(unit.spacing, 6)
276  inner_h = affine.scale(outer_h, 1/np.sqrt(7), 1/np.sqrt(7))
277  if unit.offset == 1:
278    inner_h = affine.rotate(inner_h, 30, (0, 0))
279  outer = tiling_utils.get_corners(outer_h)
280  inner = tiling_utils.get_corners(inner_h)
281  if unit.offset == 1:
282    o = []
283    for p1, p2 in zip(outer[:-1], outer[1:]):
284      o.extend([p1, geom.Point([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2])])
285    i = []
286    for p1, p2 in zip(inner[:-1], inner[1:]):
287      i.extend([p1, geom.Point([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2])])
288    return [
289      inner_h,
290      geom.Polygon([i[2], i[0], o[11], o[0], o[1]]),
291      geom.Polygon([i[4], i[2]] + o[1:4]),
292      geom.Polygon([i[6], i[4]] + o[3:6]),
293      geom.Polygon([i[8], i[6]] + o[5:8]),
294      geom.Polygon([i[10], i[8]] + o[7:10]),
295      geom.Polygon([i[0], i[10]] + o[9:])
296    ]
297  else:
298    return [
299      inner_h,
300      geom.Polygon([inner[1], inner[0], outer[0], outer[1]]),
301      geom.Polygon([inner[2], inner[1], outer[1], outer[2]]),
302      geom.Polygon([inner[3], inner[2], outer[2], outer[3]]),
303      geom.Polygon([inner[4], inner[3], outer[3], outer[4]]),
304      geom.Polygon([inner[5], inner[4], outer[4], outer[5]]),
305      geom.Polygon([inner[0], inner[5], outer[5], outer[0]])
306    ]
def get_9_parts_of_hexagon( unit: weavingspace.tile_unit.TileUnit) -> list[shapely.geometry.polygon.Polygon]:
309def get_9_parts_of_hexagon(unit: TileUnit) -> list[geom.Polygon]:
310  c = geom.Point(0, 0)
311  outer_h = tiling_utils.get_regular_polygon(unit.spacing, 6)
312  inner_h = affine.scale(outer_h, 1/np.sqrt(3), 1/np.sqrt(3))
313  if unit.offset == 1:
314    inner_h = affine.rotate(inner_h, 30, (0, 0))
315  outer = tiling_utils.get_corners(outer_h)
316  inner = tiling_utils.get_corners(inner_h)
317  if unit.offset == 1:
318    o = []
319    for p1, p2 in zip(outer[:-1], outer[1:]):
320      o.extend([p1, geom.Point([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2])])
321    i = []
322    for p1, p2 in zip(inner[:-1], inner[1:]):
323      i.extend([p1, geom.Point([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2])])
324    return [
325      geom.Polygon([c, i[1], i[2], i[4], i[5]]),
326      geom.Polygon([c, i[5], i[6], i[8], i[9]]),
327      geom.Polygon([c, i[9], i[10], i[0], i[1]]),
328      geom.Polygon([o[0], o[1], i[2], i[0], o[11]]),
329      geom.Polygon([o[2], o[3], i[4], i[2], o[1]]),
330      geom.Polygon([o[4], o[5], i[6], i[4], o[3]]),
331      geom.Polygon([o[6], o[7], i[8], i[6], o[5]]),
332      geom.Polygon([o[8], o[9], i[10], i[8], o[7]]),
333      geom.Polygon([o[10], o[11], i[0], i[10], o[9]])
334    ]
335  else:
336    return [
337      geom.Polygon([c, inner[0], inner[1], inner[2]]),
338      geom.Polygon([c, inner[2], inner[3], inner[4]]),
339      geom.Polygon([c, inner[4], inner[5], inner[0]]),
340      geom.Polygon([inner[1], inner[0], outer[0], outer[1]]),
341      geom.Polygon([inner[2], inner[1], outer[1], outer[2]]),
342      geom.Polygon([inner[3], inner[2], outer[2], outer[3]]),
343      geom.Polygon([inner[4], inner[3], outer[3], outer[4]]),
344      geom.Polygon([inner[5], inner[4], outer[4], outer[5]]),
345      geom.Polygon([inner[0], inner[5], outer[5], outer[0]])
346    ]
def setup_laves(unit: weavingspace.tile_unit.TileUnit) -> None:
349def setup_laves(unit:TileUnit) -> None:
350  """The Laves tilings. See https://en.wikipedia.org/wiki/List_of_Euclidean_uniform_tilings#Laves_tilings.
351
352  These are all isohedral, but mostly not regular polygons. We
353  prioritise them over the Archimedean tilings because being
354  isohedral all tiles are the same size. Several are hex
355  dissections and setup is delegated accordingly.
356
357  Args:
358    unit (TileUnit):  the TileUnit to setup.
359  """
360  if unit.code == "3.3.3.3.3.3":
361    # this is the regular hexagons
362    _setup_base_tile(unit, TileShape.HEXAGON)
363    _setup_none_tile(unit)
364    return
365  if unit.code == "3.3.3.3.6":
366    # this one needs its own code
367    _setup_laves_33336(unit)
368    return
369  elif unit.code == "3.3.3.4.4":
370    print(f"The code [{unit.code}] is unsupported.")
371  elif unit.code == "3.3.4.3.4":
372    # king of tilings!
373    setup_cairo(unit)
374    return
375  elif unit.code == "3.4.6.4":
376    # the hex 6-dissection
377    unit.n = 6
378    unit.offset = 1
379    setup_hex_slice(unit)
380    return
381  elif unit.code == "3.6.3.6":
382    # hex 3-dissection (also a cube weave!)
383    unit.n = 3
384    unit.offset = 0
385    setup_hex_slice(unit)
386    return
387  elif unit.code == "3.12.12":
388    # again this one needs its own
389    _setup_laves_31212(unit)
390    return
391  elif unit.code == "4.4.4.4":
392    # square grid
393    _setup_base_tile(unit, TileShape.RECTANGLE)
394    _setup_none_tile(unit)
395    return
396  elif unit.code == "4.6.12":
397    # hex 12-dissection
398    unit.n = 12
399    unit.offset = 0
400    setup_hex_slice(unit)
401    return
402  elif unit.code == "4.8.8":
403    # this one needs its own (a 4-dissection of the square)
404    # perhaps to be added as a category later...
405    _setup_laves_488(unit)
406    return
407  elif unit.code == "6.6.6":
408    # triangles
409    _setup_base_tile(unit, TileShape.TRIANGLE)
410    _setup_none_tile(unit)
411  else:
412    print(f"[{unit.code}] is not a valid Laves code.")
413
414  unit.tiling_type = None
415  _setup_none_tile(unit)
416  return

The Laves tilings. See https://en.wikipedia.org/wiki/List_of_Euclidean_uniform_tilings#Laves_tilings.

These are all isohedral, but mostly not regular polygons. We prioritise them over the Archimedean tilings because being isohedral all tiles are the same size. Several are hex dissections and setup is delegated accordingly.

Args: unit (TileUnit): the TileUnit to setup.

def setup_archimedean(unit: weavingspace.tile_unit.TileUnit) -> None:
491def setup_archimedean(unit:TileUnit) -> None:
492  """The Archimedean 'regular tilings. See https://en.wikipedia.org/wiki/List_of_Euclidean_uniform_tilings#Convex_uniform_tilings_of_the_Euclidean_plane
493
494  Many of these are most easily constructed as duals of the Laves tilings.
495
496  Some are not yet implemented:
497
498  (3.3.3.4.4) is kind of weird (stripes of triangles and squares) so can't be
499  bothered with it. Perhaps as a 5-variable option we'll get to it in time.
500
501  Args:
502    unit (TileUnit):  the TileUnit to setup.
503  """
504  if unit.code == "3.3.3.3.3.3":
505    _setup_base_tile(unit, TileShape.TRIANGLE)
506    _setup_none_tile(unit)
507    return
508  if unit.code == "3.3.3.3.6":
509    setup_laves(unit)
510    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
511    unit.setup_regularised_prototile_from_tiles()
512    return
513  elif unit.code == "3.3.3.4.4":
514    print(f"The code [{unit.code}] is unsupported.")
515  elif unit.code == "3.3.4.3.4":
516    # this is an attractive 6-colourable triangles and squares tiling
517    setup_laves(unit)
518    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
519    unit.setup_regularised_prototile_from_tiles()
520    return
521  elif unit.code == "3.4.6.4":
522    setup_laves(unit)
523    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
524    unit.setup_regularised_prototile_from_tiles()
525    return
526  elif unit.code == "3.6.3.6":
527    setup_laves(unit)
528    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
529    unit.setup_regularised_prototile_from_tiles()
530    return
531  elif unit.code == "3.12.12":
532    # nice! we can make dodecagons without having to think too hard
533    # simply use the dual code. (Although really... it probably
534    # would've been easier to make the dodecagon... other than
535    # calculating the scale relative to the hexagon base tile!)
536    setup_laves(unit)
537    unit.setup_vectors()
538    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
539    unit.setup_regularised_prototile_from_tiles()
540    return
541  elif unit.code == "4.4.4.4":
542    _setup_base_tile(unit, TileShape.RECTANGLE)
543    _setup_none_tile(unit)
544    return
545  elif unit.code == "4.6.12":
546    # more dodecagons for free!
547    setup_laves(unit)
548    unit.setup_vectors()
549    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
550    unit.setup_regularised_prototile_from_tiles()
551    return
552  elif unit.code == "4.8.8":
553    # this is the octagon and square tiling
554    setup_laves(unit)
555    unit.tiles = tiling_utils.get_dual_tile_unit(unit)
556    unit.setup_regularised_prototile_from_tiles()
557    return
558  elif unit.code == "6.6.6":
559    _setup_base_tile(unit, TileShape.HEXAGON)
560    _setup_none_tile(unit)
561    return
562  else:
563    print(f"[{unit.code}] is not a valid Laves code.")
564
565  unit.tiling_type = None
566  _setup_none_tile(unit)
567  return

The Archimedean 'regular tilings. See https://en.wikipedia.org/wiki/List_of_Euclidean_uniform_tilings#Convex_uniform_tilings_of_the_Euclidean_plane

Many of these are most easily constructed as duals of the Laves tilings.

Some are not yet implemented:

(3.3.3.4.4) is kind of weird (stripes of triangles and squares) so can't be bothered with it. Perhaps as a 5-variable option we'll get to it in time.

Args: unit (TileUnit): the TileUnit to setup.

def setup_hex_colouring(unit: weavingspace.tile_unit.TileUnit) -> None:
604def setup_hex_colouring(unit:TileUnit) -> None:
605  """3, 4, and 7 colourings of a regular array of hexagons.
606
607  Args:
608    unit (TileUnit):  the TileUnit to setup.
609  """
610  hexagon = tiling_utils.get_regular_polygon(unit.spacing / np.sqrt(unit.n), 6)
611  if unit.n == 3:
612    # Point up hex at '*' displaced to 3 positions:
613    #      2
614    #      *
615    #    3   1
616    _setup_base_tile(unit, TileShape.HEXAGON)
617    hexagon = affine.rotate(hexagon, 30, origin = (0, 0))
618    # Copy and translate to alternate corners
619    corners = [p for i, p in enumerate(hexagon.exterior.coords)
620           if i in (0, 2, 4)]
621    hexes = [affine.translate(hexagon, p[0], p[1]) for p in corners]
622  elif unit.n == 4:
623    # Point up hex at '*' displaced to 4 positions:
624    #      2
625    #     3*1
626    #      4
627    _setup_base_tile(unit, TileShape.DIAMOND)
628    hexagon = affine.rotate(hexagon, 30, origin = (0, 0))
629    hex1 = affine.translate(hexagon, unit.spacing / 4, 0)
630    hex2 = affine.translate(hexagon, 0, unit.spacing * np.sqrt(3) / 4)
631    hex3 = affine.translate(hexagon, -unit.spacing / 4, 0)
632    hex4 = affine.translate(hexagon, 0, -unit.spacing * np.sqrt(3) / 4)
633    hexes = [hex1, hex2, hex3, hex4]
634  elif unit.n == 7:  # the 'H3' tile
635    # Make a hexagon and displace in the direction of its
636    # own 6 corners, scaled as needed
637    _setup_base_tile(unit, TileShape.HEXAGON)
638    rotation = np.degrees(np.arctan(1 / 3 / np.sqrt(3)))
639    corners = [p for p in hexagon.exterior.coords][:-1]
640    hexagon = affine.rotate(hexagon, 30)
641    hexes = [hexagon] + [affine.translate(
642      hexagon, x * np.sqrt(3), y * np.sqrt(3)) for x, y in corners]
643    hexes = [affine.rotate(h, rotation, origin = (0, 0))
644             for h in hexes]
645  else:
646    _setup_base_tile(unit, TileShape.HEXAGON)
647    _setup_none_tile(unit)
648    return
649
650  unit.tiles = gpd.GeoDataFrame(
651    data = {"tile_id": list(string.ascii_letters)[:unit.n]},
652    crs = unit.crs,
653    geometry = gpd.GeoSeries(hexes))
654  unit.setup_regularised_prototile_from_tiles()

3, 4, and 7 colourings of a regular array of hexagons.

Args: unit (TileUnit): the TileUnit to setup.

def setup_square_colouring(unit: weavingspace.tile_unit.TileUnit) -> None:
657def setup_square_colouring(unit:TileUnit) -> None:
658  """Colourings of a regular array of squares. Only supports n = 5 at present
659  but we need a n=5 option
660
661  Args:
662    unit (TileUnit):  the TileUnit to setup.
663  """
664  sq = tiling_utils.get_regular_polygon(unit.spacing, 4)
665  if unit.n == 5:
666    _setup_base_tile(unit, TileShape.RECTANGLE)
667
668    # Copy and translate square
669    tr = [(0, 0), (unit.spacing, 0), (0, unit.spacing),
670        (-unit.spacing, 0), (0, -unit.spacing)]
671    squares = [affine.translate(sq, v[0], v[1]) for v in tr]
672    squares = [affine.scale(sq, 1 / np.sqrt(5), 1 / np.sqrt(5),
673                origin = (0, 0)) for sq in squares]
674    rotation = np.degrees(np.arctan2(1, 2))
675    squares = [affine.rotate(sq, rotation, origin = (0, 0))
676           for sq in squares]
677  else:
678    _setup_base_tile(unit, TileShape.RECTANGLE)
679    _setup_none_tile(unit)
680    return
681
682  unit.tiles = gpd.GeoDataFrame(
683    data = {"tile_id": list(string.ascii_letters)[:unit.n]},
684    crs = unit.crs,
685    geometry = gpd.GeoSeries(squares))
686  unit.setup_regularised_prototile_from_tiles()

Colourings of a regular array of squares. Only supports n = 5 at present but we need a n=5 option

Args: unit (TileUnit): the TileUnit to setup.