DEV Community

Black
Black

Posted on

Hyperbolic tiling in Python: draw Escher's Poincare disc and 3D print it

Escher's Circle Limit prints show a hyperbolic tiling squeezed into a disc, with tiles shrinking toward the rim. The maths is short: a tile edge is a circle that meets the unit circle at right angles, and the next tile is the mirror image of the current one across that circle. Mirroring across a circle is an inversion. That is all you need to draw the {7,3} tiling (heptagons, three at every corner) and print it as a flat ornament with manifold3d.

1. The first heptagon

For a {p,q} tiling, the vertices of the central polygon sit at a distance from the centre given by a closed formula:

import math, cmath
p, q = 7, 3
r0 = math.sqrt(math.cos(math.pi/p + math.pi/q) / math.cos(math.pi/p - math.pi/q))
V = [r0 * cmath.exp(2j * math.pi * k / p) for k in range(p)]
Enter fullscreen mode Exit fullscreen mode

2. Edge circle and inversion

The geodesic through two points u and v is a circle with centre c and radius R, where |c|^2 = R^2 + 1 (that is the right-angle condition). Solve two linear equations for c:

def circ(u, v):
    a1, b1, c1 = u.real, u.imag, (abs(u)**2 + 1) / 2
    a2, b2, c2 = v.real, v.imag, (abs(v)**2 + 1) / 2
    det = a1*b2 - a2*b1
    c = complex((c1*b2 - c2*b1) / det, (a1*c2 - a2*c1) / det)
    return c, math.sqrt(abs(c)**2 - 1)

def refl(z, c, R):                 # inversion in the circle (c, R)
    d = z - c
    return c + R*R * d / abs(d)**2
Enter fullscreen mode Exit fullscreen mode

3. Grow the tiling

Reflect each polygon across each of its edges. Skip polygons you have already built, which you detect by the rounded sum of their vertices.

polys, seen, frontier = [V], {round(sum(V), 3)}, [V]
for _ in range(3):                         # 3 rings of neighbours
    nxt = []
    for P in frontier:
        for i in range(p):
            c, R = circ(P[i], P[(i+1) % p])
            N = [refl(z, c, R) for z in P]
            k = complex(round(sum(N).real, 3), round(sum(N).imag, 3))
            if k not in seen:
                seen.add(k); polys.append(N); nxt.append(N)
    frontier = nxt
Enter fullscreen mode Exit fullscreen mode

Three rings give 85 heptagons.

4. Edges become printable lines

Walk each polygon edge as a circular arc, cut it into 12 short straight pieces, and thicken each piece into a thin rectangle with CrossSection. Union everything, add an outer rim, extrude 2.4 mm.

Gotchas

  • Stop the tiling at radius 0.93. Past that the tiles are smaller than a nozzle.
  • Put the rim at that same radius. If it sits further out, the lattice and the rim are separate islands and decompose() throws one away.
  • Make line width depend on distance from the centre, but never below 0.65 mm half-width.
  • {5,4}, {4,5} and {3,7} work with the same code: change p, q and the loop depth.

The finished {7,3} ornament and a {5,4}/{4,5}/{3,7} 3-pack are on Cults3D for $1.27. I post more of these experiments on Patreon.

Top comments (0)