Skip to content

geomotif.bases.tiling

Bases for tilings: one cell repeated on a lattice, or tiles that subdivide.

The two cover the field between them. :class:LatticeTiling is the periodic case -- square, triangular, hexagonal, rhombille, Cairo, herringbone, Truchet -- where a single cell is stamped along two basis vectors. :class:Substitution Tiling is the aperiodic case -- Penrose, Ammann-Beenker, girih -- where a handful of seed tiles are replaced by smaller copies of themselves, over and over.

Classes:

Name Description
LatticeTiling

Base for a periodic tiling: one cell, repeated on two basis vectors.

SubstitutionTiling

Base for an aperiodic tiling: seed tiles, subdivided :attr:depth times.

LatticeTiling dataclass

LatticeTiling(*, region: Bounds, clip: bool = True)

Bases: Motif, ABC

Base for a periodic tiling: one cell, repeated on two basis vectors.

Implement :meth:cell (the geometry of one tile) and :meth:basis (the two translations that repeat it), and give the motif a :attr:region to fill::

@register("tiling.square", family="tiling")
@dataclass(frozen=True, slots=True)
class SquareTiling(LatticeTiling):
    size: float = 10.0

    def basis(self) -> tuple[Point, Point]:
        return ((self.size, 0.0), (0.0, self.size))

    def cell(self) -> Design:
        s = self.size
        return Design((Path(((0, 0), (s, 0), (s, s), (0, s)), closed=True),))

:meth:basis is a method rather than a field because for most tilings the vectors follow from the motif's own parameters, as above; a tiling that genuinely wants caller-supplied vectors can declare a field and return it.

Methods:

Name Description
basis

Return the two translation vectors that generate the lattice.

cell

Return the geometry of a single cell, at lattice origin.

basis abstractmethod

basis() -> tuple[Point, Point]

Return the two translation vectors that generate the lattice.

Source code in src/geomotif/bases/tiling.py
@abstractmethod
def basis(self) -> tuple[Point, Point]:
    """Return the two translation vectors that generate the lattice."""

cell abstractmethod

cell() -> Design

Return the geometry of a single cell, at lattice origin.

Source code in src/geomotif/bases/tiling.py
@abstractmethod
def cell(self) -> Design:
    """Return the geometry of a single cell, at lattice origin."""

SubstitutionTiling dataclass

SubstitutionTiling(*, depth: int = 4)

Bases: Motif, ABC

Base for an aperiodic tiling: seed tiles, subdivided :attr:depth times.

The tile type is yours -- a dataclass of three vertices, a rhomb with an orientation, whatever the substitution rule needs. The base only ever passes tiles back to your own methods, so it never has to know.

Implement :meth:seed (the starting tiles), :meth:subdivide (one tile to its replacements) and :meth:outline (a tile to the strokes that draw it).

Notes

Tile count grows geometrically -- a rule with three replacements reaches a hundred thousand tiles by depth eleven -- so the expansion is capped and raises rather than exhausting memory.

Shared edges are drawn once per tile that owns them, so a plotter will trace most edges twice. Deduplicating them means comparing floating-point vertices for equality, which is a judgement call about tolerance the base should not be making for you.

Methods:

Name Description
seed

Return the tiles the subdivision starts from.

subdivide

Return the tiles that replace tile in the next round.

outline

Return the strokes that draw tile.

tiles

Return the seed tiles subdivided :attr:depth times.

seed abstractmethod

seed() -> Iterable[TileT]

Return the tiles the subdivision starts from.

Source code in src/geomotif/bases/tiling.py
@abstractmethod
def seed(self) -> Iterable[TileT]:
    """Return the tiles the subdivision starts from."""

subdivide abstractmethod

subdivide(tile: TileT) -> Iterable[TileT]

Return the tiles that replace tile in the next round.

Source code in src/geomotif/bases/tiling.py
@abstractmethod
def subdivide(self, tile: TileT) -> Iterable[TileT]:
    """Return the tiles that replace ``tile`` in the next round."""

outline abstractmethod

outline(tile: TileT) -> Iterable[Path]

Return the strokes that draw tile.

Source code in src/geomotif/bases/tiling.py
@abstractmethod
def outline(self, tile: TileT) -> Iterable[Path]:
    """Return the strokes that draw ``tile``."""

tiles

tiles() -> tuple[TileT, ...]

Return the seed tiles subdivided :attr:depth times.

Exposed separately from :meth:build because the tiles themselves are often what you want -- to count them, to check a substitution rule preserves area, or to color them by type.

Source code in src/geomotif/bases/tiling.py
def tiles(self) -> tuple[TileT, ...]:
    """Return the seed tiles subdivided :attr:`depth` times.

    Exposed separately from :meth:`build` because the tiles themselves are
    often what you want -- to count them, to check a substitution rule
    preserves area, or to color them by type.
    """
    if self.depth < 0:
        raise ValueError(f"depth must be >= 0, got {self.depth}")

    current = tuple(self.seed())
    if not current:
        raise ValueError(f"{type(self).__name__}.seed() returned no tiles")

    for round_number in range(self.depth):
        current = tuple(child for tile in current for child in self.subdivide(tile))
        if not current:
            raise ValueError(
                f"{type(self).__name__}.subdivide() emptied the tiling in round "
                f"{round_number + 1}: every tile must be replaced by at least one tile"
            )
        if len(current) > _MAX_TILES:
            raise ValueError(
                f"{type(self).__name__} expanded to {len(current)} tiles after "
                f"{round_number + 1} of {self.depth} rounds (limit {_MAX_TILES}); "
                f"use a smaller depth"
            )
    return current