DEV Community

Cover image for A true over-under Celtic knot and tree-ring coasters in Python (3D printable)
Black
Black

Posted on

A true over-under Celtic knot and tree-ring coasters in Python (3D printable)

Two flat prints, two small pieces of math that make them look right: a Celtic knot trivet with a true over-under weave, and a set of tree-ring coasters where every slice grows from its own random seed. Both are generated in Python with manifold3d and print with no supports.

1. Celtic knot trivet: getting over-under-over from one sine

Celtic knot trivet

The band follows a (3, 8) torus-knot path flattened onto the plane:

P, Q = 3, 8; R0, A = 50.0, 35.0
t = np.linspace(0, 2*np.pi, 1200, endpoint=False)
r = R0 + A*np.cos(Q*t); a = P*t
xy = np.c_[r*np.cos(a), r*np.sin(a)]
s = np.sin(2*Q*t)      # height sign
Enter fullscreen mode Exit fullscreen mode

The trick is sin(2Qt). Each strand meets another strand twice per lobe, and sin(2Qt) changes sign exactly between consecutive crossings. If height is BASE + RISE * s, a strand goes over, then under, then over, all the way around. That is the rule that makes a Celtic knot read as woven instead of a flat tangle.

A height field alone still looks muddy at the crossings, because the low strand runs straight through the high one. So the model is built in two halves:

  • the "over" segments (s > 0) at full height
  • the "under" segments, with a widened copy of the over strand subtracted from them near each crossing
cutter = band(xy, np.full(N, 3.0), s > 0.55, W + 1.1).translate([0, 0, FLOOR])
m = (under - cutter) + over
Enter fullscreen mode Exit fullscreen mode

The cutter starts 1.6 mm above the bed, not at zero. That leaves a thin floor under every crossing, so the band stays one connected piece and does not fall apart into separate arcs. A small groove along the top of the band and two rim rings finish it.

Celtic Knot Trivet on Payhip

2. Tree ring coasters: rings from a cumulative sum

Tree ring coasters

Real growth rings are not evenly spaced: wet years are wide, dry years are narrow. That is one line of numpy:

years = np.cumsum(rng.uniform(1.4, 3.4, 40))   # ring radii
Enter fullscreen mode Exit fullscreen mode

For every grid point, measure the distance from an off-centre pith, add a little angular wobble, then find which two rings it sits between with searchsorted:

q = rp + wobble
k = np.searchsorted(years, q).clip(1, len(years) - 1)
d = np.minimum(abs(q - years[k]), abs(q - years[k-1]))
h = T - 0.85*np.exp(-(d / 0.5)**2)           # dark late-wood line
Enter fullscreen mode Exit fullscreen mode

This is vectorized over the whole grid, so a 96 mm slice at 0.3 mm resolution meshes in seconds. Drying cracks are V-shaped cuts that are widest at the bark and fade toward the centre, and the bark rim is a few summed sines at prime frequencies so it never visibly repeats.

One thing that cost me a render: the groove profile needs a width (sigma) of at least 0.5 mm at a 0.3 mm grid step. Narrower than that and the lines alias into dotted noise on the mesh.

Each of the four coasters uses a different seed, so no two match, and they can print in three colours with filament swaps at fixed heights.

Tree Ring Coasters on Payhip

Takeaways

  • Look for the one function that encodes the rule (sin(2Qt) for the weave, cumsum for the years) before adding detail.
  • When cutting one part with another, leave a floor or bridge so the result stays one piece. Check with decompose().
  • Numpy SDFs on a grid are far faster than per-point Python functions.

Both designs are licensed so you can print them and sell the prints; the files themselves stay mine. More of my designs: Cults3D ยท patreon.com/123black

Top comments (0)