cadloop pen holes, refined models/spirograph.scad · +78 −27, one file

the report

The spirograph disks or objects that the pen will go into need to be refined. When I print them they are too narrow for a pen to go through. I think the original spirograph had a countersunk shape around each pen-hole so it was easier to draw with. Richard, 4 August 2026

Two separate faults in one sentence. The hole is too small, and its top edge is a square-cut rim that a pen tip has to be aimed into rather than dropped into. The model had them as plain 2.4 mm circles cut straight through a 4 mm plate.

before Cross-section of a pen hole: a plain straight bore with square top and bottom edges
Section through hole 1 of the 36T wheel. 2.4 mm straight through, square edges top and bottom.
after Cross-section of the same pen hole: a wider bore with a cone opening out at the top face
Same cut, same camera. 3.0 mm bore opening to 4.0 mm across the top 1.0 mm.

Both images are projection(cut = true) through the plane y = 0, which passes exactly through the centre of pen hole 1. The notches along the top edge are the debossed hole number, cut by the same plane. Nothing here is drawn by hand.

the bore

The first fault is the one that actually stops a pen. A hole modelled at 2.4 mm does not print at 2.4 mm: the perimeters are laid down on the inside of the path and squash inward, so a small hole always comes out undersize. 2.4 was already at the edge; the print pushed it over.

/* [Wheels] */
-pen_hole_d      = 2.4;   // suits a 0.5 to 0.7 mm gel pen. Measure yours
+pen_hole_d      = 3.0;   // bore. 2.4 measured too tight once printed. Measure yours

This stays the calibration knob. Everything downstream is derived from it — how far the outermost hole sits from the rim (pen_r_max), how many holes each wheel gets (pen_count), and the spacing rule added in step 5 — so if 3.0 still binds on your printer, change this one number and the rest re-derives. Nothing else is hard-coded to it.

modelled bore, before2.4 mm
modelled bore, after3.0 mm
plate thickness4.0 mm
holes on the 24T wheel5
holes on the 36T wheel11
holes on the 80T wheel24 (capped)

One caveat, stated plainly: 3.0 mm is an estimate for a gel pen after shrinkage, not a measurement of your printer. The report said "too narrow", it did not say by how much. First print is the test.

the funnel

The second fault is the one you named. A shop-bought spirograph countersinks every hole so the pen tip finds the hole on its own and can lean without its shoulder catching on the rim. The old model cut a flat circle out of a 2D outline before extruding it, which cannot express a funnel at all, so the hole became a 3D cut instead.

// A straight bore with a funnel around the top, the way a shop-bought
// spirograph is countersunk: the pen tip drops in instead of being aimed,
// and it can lean without the printed rim catching on its shoulder.
// Cut in 3D, so every wheel subtracts this rather than a flat circle.
module pen_bore() {
    translate([0, 0, -0.5])
        cylinder(d = pen_hole_d, h = wheel_thickness + 1, $fn = 24);
    translate([0, 0, wheel_thickness - pen_cs_depth])
        cylinder(d1 = pen_hole_d, d2 = pen_hole_d + 2 * pen_cs_rim,
                 h = pen_cs_depth + 0.01, $fn = 24);
}

One module, subtracted by both the circular wheels and the three non-circular shapes. The old code had the hole geometry written out twice, once in pen_spiral() and once inline in nc_wheel(); a fix applied to one of them would have left the other still wrong. That is why the diff deletes as well as adds.

pen_cs_rim — flare per side0.5 mm
pen_cs_depth — how deep it cuts1.0 mm
opening at the top face4.0 mm
cone half-angle26.6°
which facethe labelled one

what it broke

A wider hole with a funnel around it needs more room than a narrow one. On the eleven circular wheels there was room to spare. On the ellipse and the trefoil there was not, and this is the part of the job that was not in the request.

The three non-circular wheels carry hand-generated hole tables laid out on a 2.5333 module grid, which at module 1.5 puts hole centres 3.8 mm apart. A 3.0 mm bore with a 4.0 mm funnel and two perimeters of wall between neighbours needs 4.8 mm. At 3.8 the funnels merge into a groove — and a pen that can slide from one hole to the next does not just look wrong, it ruins the drawing.

before Ellipse wheel with twenty pen holes packed close together, their numbers overlapping
ellipse, 20 holes at 3.8 mm centres. The numbers collide too, which was the visible symptom of the same crowding.
after Ellipse wheel with ten countersunk pen holes, evenly spaced, numbers legible
ellipse, 10 holes at 7.6 mm centres, countersunk. Both ends of each long ray kept.
ellipse20 → 10 holes
trefoil14 → 8 holes
egg — already 4.98 mm apart8 → 8 holes
eleven circular wheelsunchanged

The spacings available on those tables are multiples of 3.8 mm, so it is all or every other — there is no arrangement that keeps 20 holes and fits a funnel. Fewer, usable holes beat more holes the pen cannot sit in. Call it the other way and it is one number to change.

the thinning

Rather than hand-edit three generated tables, the model drops the crowding itself. It walks each table backwards and keeps a hole only when it clears everything kept so far.

function pen_pitch_min() = pen_hole_d + 2 * pen_cs_rim + 0.8;

function crowded(h, kept) =
    len([ for (k = kept)
            if (gear_module * norm([k[0] - h[0], k[1] - h[1]]) < pen_pitch_min())
                1 ]) > 0;

function thin(hs, i = 0, kept = []) =
    i >= len(hs) ? kept
                 : thin(hs, i + 1,
                        crowded(hs[i], kept) ? kept : concat(kept, [hs[i]]));

function nc_holes(sh) = rev(thin(rev(sh[6])));

Backwards matters. Each ray in those tables is listed inner to outer, and the outermost hole is the one that draws the widest figure, so working from the end of the list is what saves it. The result is reversed back so the numbering still counts outward from the centre, the way the rest of the set does. The threshold is derived, not typed: bore + flare + 0.8 mm of wall, where 0.8 is two perimeters at a 0.4 mm nozzle.

Because it is derived, it undoes itself. Raise gear_module to 2.0 and the same tables come out at 5.07 mm centres, above the threshold, and every dropped hole comes back with no edit.

the check

Two funnels running into each other is a fault no render, no slicer and no bed check would report. The geometry stays a clean manifold, it fits the plate, it slices. It just draws badly. So the model proves the spacing on every render.

for (t = wheel_teeth)
    assert(min_pitch(wheel_hole_pts(t)) >= pen_pitch_min(),
           str(t, "T: pen holes are closer than their countersinks are wide"));
for (s = nc_shapes)
    assert(min_pitch(nc_hole_pts(s)) >= pen_pitch_min(),
           str(s[0], ": pen holes still crowd after thinning"));

An assertion that has never failed is not evidence of anything, so it was made to fail on purpose — squeeze the spiral and the 36 wheels and three shapes are checked, and the smallest wheel is the first to go:

$ openscad -D "pen_spiral_dr=0.4" models/spirograph.scad
ERROR: Assertion '(min_pitch(wheel_hole_pts(t)) >= pen_pitch_min())' failed:
       "24T: pen holes are closer than their countersinks are wide"
       in file spirograph.scad, line 484

This is the step the walkthrough on the main page calls verify, and the reason it exists: the checks that matter for a part that has to work are the ones you have to write yourself.

the proof

Everything below ran after the change, on this machine.

before 36 tooth wheel with narrow straight-walled pen holes
36T, 2.4 mm holes, square rims.
after 36 tooth wheel with wider countersunk pen holes showing a funnel around each
36T, 3.0 mm holes, countersunk. Same camera, same wheel.
$ python -m cadloop.gearcheck
24T         24     0.000000  pass
30T         30     0.000000  pass
                  ...
80T         80     0.000000  pass
ellipse     27     0.000000  pass
egg         20     0.000000  pass
trefoil     23     0.000000  pass

14/14 parts mesh cleanly
no parts overlap on the sheet
full sheet rendersmanifold, NoError
parts meshing14 / 14
sheet collisionsnone
spacing assertionspass, and proven to fail
files touched1
diff+78 −27

Not checked, and not checkable from here: whether 3.0 mm is right for your pen on your printer. That needs one wheel printed. If it still binds, raise pen_hole_d — the hole count, the rim clearance and the thinning all follow it.