DEV Community

Cover image for Gosper curve in Python: a space-filling L-system ornament for 3D printing
Black
Black

Posted on Edited on

Gosper curve in Python: a space-filling L-system ornament for 3D printing

A space-filling curve visits every cell of a grid exactly once without crossing itself. The Gosper curve (flowsnake) does it on a hexagonal grid, and you can draw it with a six-line L-system. This post turns it into a flat, 3D-printable ornament with manifold3d.

The L-system

Both symbols mean "step forward". Turn 60 degrees left or right between steps.

import math
rules = {'A': 'A-B--B+A++AA+B-', 'B': '+A-BB--B-A++A+B'}
s = 'A'
for _ in range(3):                 # 3 rounds = 343 steps
    s = ''.join(rules.get(c, c) for c in s)

ang = x = y = 0.0
pts = [(0.0, 0.0)]
for c in s:
    if c in 'AB':
        x += math.cos(math.radians(ang))
        y += math.sin(math.radians(ang))
        pts.append((x, y))
    elif c == '+':
        ang += 60
    elif c == '-':
        ang -= 60
Enter fullscreen mode Exit fullscreen mode

From path to printable plate

Thicken each step into a thin rectangle, union them all, and extrude 2.4 mm.

import manifold3d as M3
from manifold3d import CrossSection

xs = [p[0] for p in pts]; ys = [p[1] for p in pts]
cx, cy = (max(xs) + min(xs)) / 2, (max(ys) + min(ys)) / 2
sc = 76 / max(max(xs) - min(xs), max(ys) - min(ys))   # fit 76 mm
P = [((a - cx) * sc, (b - cy) * sc) for a, b in pts]

w = 0.75                           # half line width: 1.5 mm line
segs, seen = [], set()
for a, b in zip(P, P[1:]):
    k = tuple(sorted((tuple(round(v, 3) for v in a), tuple(round(v, 3) for v in b))))
    if k in seen:                  # curve reuses some edges
        continue
    seen.add(k)
    dx, dy = b[0] - a[0], b[1] - a[1]
    seg = CrossSection.square([math.hypot(dx, dy) + 2 * w, 2 * w], True)
    segs.append(seg.rotate(math.degrees(math.atan2(dy, dx)))
                   .translate([(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]))

plate = CrossSection.batch_boolean(segs, M3.OpType.Add)
plate = max(plate.decompose(), key=lambda c: c.area())  # drop loose bits
solid = plate.extrude(2.4)
Enter fullscreen mode Exit fullscreen mode

The + 2 * w in the rectangle length makes neighbouring segments overlap at the joints, so the path stays one piece.

Gotchas

  • Round coordinates before hashing edges, or floating-point noise defeats the duplicate check.
  • Keep the gap between lines at least 1.5 times the nozzle width (0.6 mm for a 0.4 mm nozzle), or the print fuses into a blob. Heighway dragon curve at 11 folds did exactly that for me; 9 folds works.
  • Same trick works in 3D: Skilling's transpose algorithm gives a 3D Hilbert curve, and Manifold.batch_hull of two spheres makes each tube segment.

Finished ornaments from this: Gosper curve and 3D Hilbert cube on Cults3D, $1.27 each.

Top comments (0)