Skip to content

geomotif.motifs.tilings

Tilings: the periodic ones, the aperiodic ones, and one that rolls dice.

Three ways of covering the plane, and the difference between them is worth knowing before you pick a class.

A periodic tiling has a unit cell and two translations that repeat it forever. Say what one cell looks like and where the next one goes, and the lattice does the rest -- that is :class:~geomotif.LatticeTiling, and it is what the square, triangular, hexagonal, rhombille, Cairo, truncated-square, snub-square and herringbone tilings here are. They need a :attr:~geomotif.LatticeTiling.region to fill, because otherwise they would run on forever.

An aperiodic tiling never repeats. There is no cell to stamp, so instead a handful of seed tiles are replaced by smaller copies of themselves, over and over -- :class:~geomotif.SubstitutionTiling. Both Penrose tilings are built that way, from the same two Robinson triangles: the kite and the dart of :class:PenroseP2 are those triangles glued along a leg, and the thin and thick rhombs of :class:PenroseP3 are the same triangles glued along their base. One pair of shapes, two famous tilings.

:class:AmmannBeenker is aperiodic too but arrives a third way, by de Bruijn's multigrid: four families of parallel lines are laid across each other, and every crossing becomes a tile. It is a plain :class:~geomotif.Motif rather than a substitution because the eightfold inflation rule needs seven tiles placed by hand in bookkeeping that is far easier to get subtly wrong than a line arrangement is, and the crossings can be checked directly: every tile it emits is a unit rhomb.

:class:TruchetTiling is periodic in its lattice and random in its contents, which is exactly the thing :class:~geomotif.LatticeTiling cannot express -- one cell, stamped everywhere. So it places its own cells, and seeds its own generator so a given seed always draws the same tiles.

Classes:

Name Description
SquareTiling

Squares edge to edge: the graph paper of tilings.

TriangularTiling

Equilateral triangles, alternately point up and point down.

HexagonalTiling

The honeycomb: regular hexagons, three to a corner.

RhombilleTiling

Tumbling blocks: each hexagon split into three rhombi.

CairoPentagonal

The Cairo tiling: pentagons in fours, spinning like a pinwheel.

TruncatedSquare

Octagons with small squares in the gaps -- the 4.8.8 tiling.

SnubSquare

Squares and triangles, two of each at every corner -- the 3.3.4.3.4.

HerringboneTiling

Rectangles laid in chevrons, each one's end against the next one's side.

TruchetTiling

Quarter-circles in square cells, each turned at random.

PenroseP3

Penrose's rhombs: one thin, one thick, and no repeating pattern ever.

PenroseP2

Penrose's kite and dart, the tiling that cannot repeat.

AmmannBeenker

The octagonal quasicrystal: squares and 45-degree rhombs, never repeating.

SquareTiling dataclass

SquareTiling(size: float = 40.0, *, region: Bounds, clip: bool = True)

Bases: LatticeTiling

Squares edge to edge: the graph paper of tilings.

Parameters:

Name Type Description Default
size float

Length of a side.

40.0

TriangularTiling dataclass

TriangularTiling(size: float = 40.0, *, region: Bounds, clip: bool = True)

Bases: LatticeTiling

Equilateral triangles, alternately point up and point down.

The cell is one of each, which together make the rhombus that repeats.

Parameters:

Name Type Description Default
size float

Length of a side.

40.0

HexagonalTiling dataclass

HexagonalTiling(size: float = 40.0, *, region: Bounds, clip: bool = True)

Bases: LatticeTiling

The honeycomb: regular hexagons, three to a corner.

Parameters:

Name Type Description Default
size float

Distance from a hexagon's middle to one of its corners, which is also the length of a side.

40.0

RhombilleTiling dataclass

RhombilleTiling(size: float = 40.0, *, region: Bounds, clip: bool = True)

Bases: LatticeTiling

Tumbling blocks: each hexagon split into three rhombi.

The oldest optical illusion in tiling. Every rhombus is a face of a cube seen in isometric projection, and which cubes stick out and which are hollow is up to the eye.

Parameters:

Name Type Description Default
size float

Side of a rhombus, which is also the hexagon's circumradius.

40.0

CairoPentagonal dataclass

CairoPentagonal(size: float = 34.0, *, region: Bounds, clip: bool = True)

Bases: LatticeTiling

The Cairo tiling: pentagons in fours, spinning like a pinwheel.

Named for the paving of Cairo's streets. The pentagon has four equal sides and one shorter, two right angles and three of 120 degrees -- the right angles are where four pentagons meet head on, and the rest is where three do.

Parameters:

Name Type Description Default
size float

Length of one of the four equal sides.

34.0

TruncatedSquare dataclass

TruncatedSquare(size: float = 34.0, *, region: Bounds, clip: bool = True)

Bases: LatticeTiling

Octagons with small squares in the gaps -- the 4.8.8 tiling.

What you get by cutting the corners off every square of a square tiling: the squares become octagons and the cut corners leave a smaller square behind, stood on its point.

Parameters:

Name Type Description Default
size float

Length of a side, shared by both shapes.

34.0

SnubSquare dataclass

SnubSquare(size: float = 34.0, *, region: Bounds, clip: bool = True)

Bases: LatticeTiling

Squares and triangles, two of each at every corner -- the 3.3.4.3.4.

The squares come in two orientations thirty degrees apart, and the triangles pair up into rhombi that fill what is left. Four triangles and two squares repeat.

Parameters:

Name Type Description Default
size float

Length of a side, shared by both shapes.

34.0

HerringboneTiling dataclass

HerringboneTiling(length: float = 60.0, width: float = 30.0, *, region: Bounds, clip: bool = True)

Bases: LatticeTiling

Rectangles laid in chevrons, each one's end against the next one's side.

The parquet floor and the brick path. Any proportion works, not only the usual two-to-one: the lattice follows from the brick.

Parameters:

Name Type Description Default
length float

The brick's two sides.

60.0
width float

The brick's two sides.

60.0

TruchetTiling dataclass

TruchetTiling(size: float = 30.0, cols: int = 10, rows: int = 10, seed: int = 0, center: Point = (0.0, 0.0))

Bases: Motif

Quarter-circles in square cells, each turned at random.

Two arcs cross every cell, joining the midpoints of its sides; whether they curl one way or the other is decided by the toss of a coin. The arcs always meet at cell borders, so what comes out is a single tangle of smooth curves -- Sebastien Truchet's tiles of 1704, and still the cheapest way to make a plotter draw something that looks designed.

Parameters:

Name Type Description Default
size float

Side of one cell.

30.0
cols int

How many cells across and down.

10
rows int

How many cells across and down.

10
seed int

Fixes the tosses. The same seed always draws the same tiles, and the generator is private to the call, so nothing else in the program can change what you get.

0
center (float, float)

Middle of the finished patch.

(0.0, 0.0)

RobinsonTriangle dataclass

RobinsonTriangle(kind: int, apex: complex, first: complex, second: complex)

Half of a Penrose tile: an isosceles triangle in the complex plane.

Two shapes only. kind 0 is the acute one, 36-72-72, whose legs are phi times its base; kind 1 is the obtuse one, 36-36-108, whose base is phi times its legs. Everything Penrose is made of these.

:attr:apex is the corner between the two legs. What :attr:first and :attr:second mean depends on the tiling: :class:PenroseP3 glues triangles along the base :attr:first--:attr:second, and :class:PenroseP2 glues them along the leg :attr:apex--:attr:first.

Complex numbers rather than points because the substitution is entirely a + (b - a) / phi -- one expression each way, instead of one per coordinate, with the rotations falling out of the arithmetic.

PenroseTiling dataclass

PenroseTiling(radius: float = 160.0, center: Point = (0.0, 0.0), *, depth: int = 4)

Bases: SubstitutionTiling[RobinsonTriangle]

Shared scaffolding for the two Penrose tilings: the seed and the scale.

The seed is ten acute triangles in a wheel, which is five whole tiles however they are glued -- five thick rhombs for :class:PenroseP3, five kites for :class:PenroseP2. Subclasses supply the substitution rule and say which edges to draw.

PenroseP3 dataclass

PenroseP3(radius: float = 160.0, center: Point = (0.0, 0.0), *, depth: int = 5)

Bases: PenroseTiling

Penrose's rhombs: one thin, one thick, and no repeating pattern ever.

Each rhombus is two Robinson triangles glued along their base, so the strokes drawn are the legs -- draw the base as well and every tile would have a line down its middle.

Parameters:

Name Type Description Default
depth int

Subdivision rounds. Tile count grows by about phi**2 a round.

5
radius float

Circumradius of the starting wheel, and so of the finished patch.

160.0
center (float, float)

Middle of the wheel.

(0.0, 0.0)

PenroseP2 dataclass

PenroseP2(radius: float = 160.0, center: Point = (0.0, 0.0), *, depth: int = 5)

Bases: PenroseTiling

Penrose's kite and dart, the tiling that cannot repeat.

Each tile is two Robinson triangles glued along a leg rather than a base: two acute ones make a kite, two obtuse ones make a dart. The same two triangles glued the other way give :class:PenroseP3, which is why both live here.

Parameters:

Name Type Description Default
depth int

Subdivision rounds. Tile count grows by about phi**2 a round.

5
radius float

Circumradius of the starting wheel, and so of the finished patch.

160.0
center (float, float)

Middle of the wheel.

(0.0, 0.0)

AmmannBeenker dataclass

AmmannBeenker(size: float = 16.0, radius: float = 150.0, offsets: tuple[float, ...] = (0.5, 0.5, 0.5, 0.5), center: Point = (0.0, 0.0))

Bases: Motif

The octagonal quasicrystal: squares and 45-degree rhombs, never repeating.

Built by de Bruijn's multigrid rather than by substitution. Four families of evenly spaced parallel lines are drawn across each other at 45 degrees; every crossing of a line from one family with a line from another names one tile, and the tile is the rhombus spanned by those two families' directions. Families a right angle apart give the squares, families 45 degrees apart give the rhombs, and there is nothing else -- which is a property you can check on the output rather than trust.

Parameters:

Name Type Description Default
size float

Edge length, shared by the square and the rhomb.

16.0
radius float

How far from the middle to tile. Tiles whose middle falls outside are dropped, so the patch comes out round.

150.0
offsets tuple of float

Where each family of lines sits relative to the origin. These choose which tiling of the family you get; every choice is locally the same as every other, and the default is the eightfold symmetric one.

(0.5, 0.5, 0.5, 0.5)
center (float, float)

Middle of the patch.

(0.0, 0.0)

Methods:

Name Description
rhombs

Return every tile as its four corners, counter-clockwise.

rhombs

rhombs() -> tuple[tuple[Point, ...], ...]

Return every tile as its four corners, counter-clockwise.

Exposed because the tiles themselves are often what you want -- to count them, to sort squares from rhombs, or to color them.

Source code in src/geomotif/motifs/tilings.py
def rhombs(self) -> tuple[tuple[Point, ...], ...]:
    """Return every tile as its four corners, counter-clockwise.

    Exposed because the tiles themselves are often what you want -- to
    count them, to sort squares from rhombs, or to color them.
    """
    n = self.families
    step = self.radius / self.size
    reach = math.ceil(step) + 2
    crossings = (2 * reach + 1) ** 2 * (n * (n - 1) // 2)
    if crossings > _MAX_RHOMBS:
        raise ValueError(
            f"{type(self).__name__} would test {crossings} line crossings "
            f"(limit {_MAX_RHOMBS}); use a larger size or a smaller radius"
        )

    directions = [(math.cos(math.pi * j / n), math.sin(math.pi * j / n)) for j in range(n)]
    cx, cy = self.center
    tiles: list[tuple[Point, ...]] = []
    for j in range(n):
        for k in range(j + 1, n):
            (jx, jy), (kx, ky) = directions[j], directions[k]
            det = jx * ky - jy * kx
            for a in range(-reach, reach + 1):
                for b in range(-reach, reach + 1):
                    pa, pb = a + self.offsets[j], b + self.offsets[k]
                    # Where the two lines cross, in units of `size`.
                    px = (pa * ky - pb * jy) / det
                    py = (pb * jx - pa * kx) / det
                    # Which side of every *other* family that crossing
                    # falls on names the tile's corner on the lattice.
                    index = [
                        math.ceil(px * dx + py * dy - self.offsets[i])
                        for i, (dx, dy) in enumerate(directions)
                    ]
                    index[j], index[k] = a, b
                    vx = math.fsum(index[i] * directions[i][0] for i in range(n))
                    vy = math.fsum(index[i] * directions[i][1] for i in range(n))
                    corners = (
                        (vx, vy),
                        (vx + jx, vy + jy),
                        (vx + jx + kx, vy + jy + ky),
                        (vx + kx, vy + ky),
                    )
                    mx = math.fsum(x for x, _ in corners) / 4.0
                    my = math.fsum(y for _, y in corners) / 4.0
                    if math.hypot(mx, my) * self.size > self.radius:
                        continue
                    tiles.append(
                        tuple((cx + x * self.size, cy + y * self.size) for x, y in corners)
                    )
    return tuple(tiles)