DEV Community

Black
Black

Posted on

Carve a jack-o'-lantern in Python with signed distance functions (manifold3d)

A jack-o'-lantern is a good first exercise for signed distance functions (SDFs): one formula gives the ribbed body, a second gives the hollow, and a few polygons carve the face. This uses manifold3d and plain Python, with no CAD program.

1. A ribbed pumpkin from one formula

A squashed ellipsoid is (rho/R)^2 + (z/H)^2 = 1. To get ribs, let the radius change with the angle: R(t) = R * (1 + 0.09 * cos(10 t)). level_set samples a function on a grid and returns a mesh where it crosses zero. Positive means inside.

import math
from manifold3d import Manifold

R, H = 30.0, 25.0

def pumpkin(scale):
    def f(x, y, z):
        rho, t = math.hypot(x, y), math.atan2(y, x)
        Rt = R * scale * (1 + 0.09 * math.cos(10 * t))
        d = math.sqrt((rho / Rt) ** 2 + (z / (H * scale)) ** 2)
        return (1 - d) * 20
    return f

box = [-36, -36, -30, 36, 36, 30]          # minx, miny, minz, maxx, maxy, maxz
outer = Manifold.level_set(pumpkin(1.0), box, 0.55)   # 0.55 mm grid
inner = Manifold.level_set(pumpkin(0.9), box, 0.55)
shell = outer - inner                       # hollow, about 2.5 mm wall
shell = shell ^ Manifold.cube([200, 200, 200]).translate([-100, -100, -17])  # flat open base
Enter fullscreen mode Exit fullscreen mode

The ^ keeps only the part above z = -17. That gives an open base for an LED tealight and a flat face to print on.

2. Carve the face

Draw each shape as a 2D polygon in the x/z plane, extrude it 50 mm and boolean-subtract it from the front wall.

from manifold3d import CrossSection

def poly(pts):
    area = sum(pts[k][0] * pts[(k + 1) % len(pts)][1]
               - pts[(k + 1) % len(pts)][0] * pts[k][1] for k in range(len(pts)))
    pts = pts if area > 0 else pts[::-1]    # must be counter-clockwise
    return CrossSection([pts]).extrude(50).rotate([90, 0, 0])

eye_l = poly([(-17, 6), (-6, 6), (-11.5, 15)])
eye_r = poly([(6, 6), (17, 6), (11.5, 15)])
nose  = poly([(-3, 2), (3, 2), (0, -3.5)])
mouth = poly([(-17, -3), (-12, -9), (-8, -5), (-4, -11), (0, -6), (4, -11),
              (8, -5), (12, -9), (17, -3), (13, -11), (0, -13), (-13, -11)])
for cut in (eye_l, eye_r, nose, mouth):
    shell = shell - cut
Enter fullscreen mode Exit fullscreen mode

3. A stem with a hang hole

stem = Manifold.cylinder(9, 4.2, 3.2, 24).translate([0, 0, H * 0.9 - 2])
hole = Manifold.cylinder(12, 1.6, 1.6, 20).rotate([90, 0, 0]).translate([0, 6, H * 0.9 + 4.5])
pumpkin_mesh = shell + (stem - hole)
Enter fullscreen mode Exit fullscreen mode

Gotchas

  • level_set calls your function with three numbers (x, y, z), not one tuple.
  • A clockwise polygon gives an empty CrossSection, and the boolean silently removes the whole shell. The signed-area check in poly fixes that.
  • Keep the bottom of the mouth well above the cut plane. A mouth that reaches the base leaves a thin floppy lip.
  • Change the rib count (6, 8, 12) and the squash H / R to get a whole family of pumpkins from the same code.

I have not test printed these yet. The meshes come out as a single manifold solid, and the renders look right. The finished lantern and a 3-pack with three shapes and faces are on Cults3D for $1.27.

Top comments (0)