A celtic knot only looks right if the band goes over, under, over, under at every crossing. Most printable "knot" coasters cheat: the strands just merge into one flat blob where they cross. I wanted a trivet that reads as a real woven band, still prints flat with no supports, and stays one solid piece.
Here is how I built it in about 50 lines of Python with manifold3d.
1. The path: a (3,8) torus knot, squashed flat
A torus knot winds P times around the axis and Q times around the tube. Pressed flat, that gives a rosette with eight petals:
P, Q = 3, 8
R0, A = 50.0, 35.0 # mean radius, petal depth (mm)
N = 1200 # samples along the band
def path():
t = np.linspace(0, 2*math.pi, N, endpoint=False)
r = R0 + A*np.cos(Q*t)
a = P*t
xy = np.c_[r*np.cos(a), r*np.sin(a)]
return xy, np.sin(2*Q*t)
2. The trick: one sine wave gives the whole weave
The second return value is the important part. Walking along the band, the crossings come at a steady rhythm, and sin(2*Q*t) changes sign between each one. So if the band's height follows that sine, every crossing has one strand high and one strand low, and they alternate all the way round. No crossing detection, no lookup table.
BASE, RISE = 5.4, 3.0
height = BASE + RISE*s # s = sin(2*Q*t)
3. Sweeping the band
Each sample gets a small "node": a cylinder with a squashed sphere on top for a rounded profile. Hulling each pair of neighbours makes a smooth swept band:
def node(p, h, w):
c = Mf.cylinder(max(h-1.4, 0.2), w, w, 24).translate([p[0], p[1], 0])
return c + Mf.sphere(1, 24).scale([w, w, 1.4]).translate([p[0], p[1], h-1.4])
def band(xy, s, keep, w=3.2):
segs = []
for i in range(N):
j = (i+1) % N
if keep[i] or keep[j]:
segs.append(Mf.batch_hull([node(xy[i], BASE+RISE*s[i], w),
node(xy[j], BASE+RISE*s[j], w)]))
return Mf.batch_boolean(segs, M3.OpType.Add)
batch_hull and batch_boolean are what keep this fast: 1200 tiny hulls unioned in one call instead of 1200 sequential booleans.
4. Making the under-strand actually go under
Heights alone are not enough, because every node is a solid column down to the bed. The low strand would still poke up through the high one. So I build the band in two halves (over and under), then cut the under half with a slightly wider copy of the over half near its peaks:
over = s > 0
A_ = band(xy, s, over)
B_ = band(xy, s, ~over)
cutter = band(xy, np.full(N, 3.0), s > 0.55, 3.2 + 1.1).translate([0, 0, 1.6])
m = (B_ - cutter) + A_
Two details matter here:
- The cutter is 1.1 mm wider than the band, so there is a visible gap on both sides of every crossing. That gap is what your eye reads as "this strand dives under".
- The cutter starts 1.6 mm above the bed. That leaves a thin floor under every crossing, so the band stays one connected piece and the whole thing prints flat.
5. Finishing touches
A shallow groove runs down the middle of the band (a chain of small hulled spheres subtracted along the path), which is what makes it look like a classic double-line celtic strap. Two flat rings frame the knot inside and out and give the trivet a stable footprint.
Result
185 mm across, 8.8 mm tall, prints flat in one piece with no supports. The pot sits on the high crossings, and the gaps let air under it.
If you want to print one, the STL is on Cults3D: Celtic Knot Trivet. The same script can make other knots by changing P, Q (Q sets the petal count), though I have only checked the weave on (3,8) so far.
Other designs and early access: patreon.com/123black

Top comments (0)