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
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
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
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.
2. Tree ring coasters: rings from a cumulative sum
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
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
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.
Takeaways
- Look for the one function that encodes the rule (
sin(2Qt)for the weave,cumsumfor 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)