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

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 

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 

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.