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()
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.
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.
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.
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 ]
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 ]
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 ]
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.
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.
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.
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.