Knot Sound Hole — any coprime leads × bights

Generator: knot_soundhole.js

Companion: plait_soundhole.md (two ribbons, even crossings)

Replaces an earlier two-lead-only generator. Set LEADS=2 and this reproduces its output exactly — verified file by file, cut layer byte-identical and the same set of engrave subpaths.

Produces a cut-ready SVG rosette for an instrument sound hole, sized in the terms a knot-tyer already uses: L leads by B bights. The strand travels round L times before it closes, and shows B scallops at the rim. Millimetre-true output, 1 user unit = 1mm.

r(theta) = R_MID + AMP*cos(B*theta/L),   theta in [0, 2*pi*L)

It generalises the two-lead generator it replaced. That one had a single parameter, Q, for its bight count, and required it to be odd — which is exactly gcd(2, B) = 1, this rule at two leads.

Leads and bights, and why they must be coprime

The strand closes into one piece iff gcd(L, B) = 1. That is not a convention, it is what the curve does: gcd(L, B) counts the separate closed pieces you end up with. Ask for 3 leads and 6 bights and you get three loops lying alongside each other, with no single ribbon to trace and no interlace to alternate over. The generator refuses, and lists the coprime bight counts near the leads you asked for.

The plait and the generator this replaced are both the L = 2 row of one table:

generatorleadsbightsgcdstrands
the retired knot generator2Q, odd11 — a knot
plait22N, even22 — a plait
this oneany Lany B coprime to L11

The structural constraint

Same as both companions. This is a cut-out: the removed material is the open area and the ribbon is what stays. A ribbon ring floating inside a round hole would drop out when the last cut closes, so the ribbon peaks deliberately overrun the rim by BITE mm and fuse the rosette into the soundboard at B anchor points. There is no continuous rim circle in the cut layer — the outer boundary is B arcs. That is correct; adding a full circle drops the rosette on the floor.

Usage

node knot_soundhole.js --help                   # every setting and its default
node knot_soundhole.js                          # report only, writes nothing
LEADS=3 BIGHTS=5 OUT=3-lead_5-bight_knot_radius30mm.svg node knot_soundhole.js
LEADS=3 BIGHTS=4 OUT=3-lead_4-bight_knot_radius30mm.svg node knot_soundhole.js
LEADS=2 BIGHTS=3 node knot_soundhole.js         # 2 leads: the classic trefoil
SELFTEST=1 node knot_soundhole.js               # check the hash, exit
DIAG=1 node knot_soundhole.js                   # per-region dump

The file that lands is not ready for the laser yet. Its geometry is, but it carries two layers and only one of them is a cut. See Output layers before you send it: the blue engrave lines must be given anything other than a cut.

--help prints this same table, so you do not need the document to hand.

OUT overwrites without asking — there is no force flag and no prompt. The two OUT= lines above name shipped sample files, so running them in this directory rewrites those files; at these parameters the bytes come back identical, but change a number and reuse the same command line and you have replaced a sample with different geometry and nothing will have said so. Write somewhere else while experimenting. The companion CLI in the living-hinge repository refuses by default, so the habits do not transfer.

VarDefaultMeaning
LEADS3times round before the strand closes
BIGHTS5scallops at the rim; must be coprime to LEADS
R_HOLE30sound hole radius, mm
AMP7.5radial swing of the weave
HW2.0ribbon half-width → ribbon is 2*HW mm wide
BITE1.5rim overrun; this is what anchors the rosette
NG1400sampling grid resolution
MIN_FEATURE0.25gaps narrower than this are welded to solid material
OUTunsetoutput path — nothing is written unless set
SELFTESTunsetverify spatial hash against brute force, then exit
DIAGunsetverbose per-region report

Neither AMP nor HW auto-scales with R_HOLE; roughly AMP ≈ 0.25*R_HOLE and HW ≈ 0.067*R_HOLE. More leads pack more ribbon into the same annulus, so expect to reduce both as LEADS goes up — though see the envelope below, because that is not enough to rescue the crowded cases.

Q is not a setting. It was the bight count of the retired two-lead generator, and because ignoring it would quietly hand back the defaults instead — Q=5 giving a 3 × 5, not a cinquefoil — it is refused with the LEADS=2 BIGHTS=5 form to type instead.

The centre opening is derived, and it can abort the run

R_CENTRE = R_HOLE + BITE - 2*(AMP + HW)

The generator exits 1 if that drops below 1mm. So shrinking R_HOLE while leaving AMP/HW at their defaults does not give you a tighter knot, it refuses to run — at defaults it aborts below R_HOLE ≈ 18.5. Scale both with the rules of thumb above and R_CENTRE ≈ 0.37*R_HOLE + BITE. The BITE term does not scale, so the ratio is not constant: 0.42 at the 30mm default, 0.40 at 50mm, 0.38 at 100mm, approaching 0.37 only well above that.

This one depends on AMP, HW, BITE and R_HOLE alone — leads and bights do not enter it.

Sampling and the error floor

The centreline is sampled at a fixed M = 20000 points — not exposed as a variable — and distance is measured to those samples, so it overestimates by up to half the sample spacing. Those M points now cover L turns rather than two, so the spacing grows with leads as well as with bights and the AMP*B/L radial derivative. It stays an order of magnitude under the 0.25mm MIN_FEATURE floor across the tested range, but the margin narrows as either number rises — and long before sampling becomes the limit, MIN_FEATURE does. See the envelope below.

Variants at R_HOLE=30 (60mm hole)

L × BRegionsCrossingsAnchorsEngraveOpen areaEquiv. plain holeNarrowest cut
2 × 3733663.3%47.73mm5.51mm
3 × 2742850.7%42.72mm2.21mm
3 × 413841649.2%42.10mm2.07mm
3 × 5161052048.2%41.64mm2.01mm
4 × 313931839.4%37.66mm0.66mm
4 × 5211553038.2%37.07mm0.61mm

2 × 3 is the trefoil, reproduced here to show the generalisation is faithful. 4 × 3 and 4 × 5 are included as warnings as much as options: both satisfy every invariant, but a 0.66mm — or 0.61mm — narrowest cut is delicate against a kerf of 0.1–0.2mm. Neither ships at this size; both ship at 39mm for the reason below.

A bigger panel alone does not fix that — and this is the trap the rules of thumb exist to prevent. The same 4 × 3 at R_HOLE = 39 with default tuning gives 0.67mm, essentially unchanged, because the ribbon did not grow with the panel and the weave got relatively tighter. Scale AMP and HW with the radius and the same knot at the same size gives 1.27mm, near enough double. That is what the shipped 4-lead_3-bight_knot_radius39mm.svg uses.

R_HOLEAMP · HWNarrowestOpen
307.5 · 2.00.66mm39.4%
397.5 · 2.00.67mm49.4%
399.75 · 2.61.27mm38.6%
3911 · 1.81.64mm55.4%

All four satisfy every invariant. The last is the most open and cuts widest, on a 3.6mm ribbon — thinner material spanning more air. The shipped file takes the middle, as the sturdiest of them.

4 × 5 is shipped at the same 39mm on the same AMP=9.75 HW=2.6, and lands at 1.25mm — within 0.02mm of what 4 × 3 gets from that tuning, despite carrying six more crossings. Adding bights at fixed leads costs the narrowest cut very little here; it is the lead count that drives it down.

More leads means less open area, because more ribbon is packed into the same ring.

Invariants — these pin the topology

crossings     == B*(L-1)
cut regions   == L*B + 1     (one centre + B*(L-1) lenses + B rim gaps)
rim anchors   == B
engrave lines == 2*B*(L-1)   (two edges of the over pass per crossing)

Two leads is the special case both older generators occupied: the retired one at (2, B) with B odd gives 2B + 1 regions, and the plait at (2, 2N) gives 4N + 1. The generator checks the crossing and region counts on every run and prints whether they match.

The tested envelope, and where it stops

Verified against every invariant at (2,3) (2,5) (2,9) (3,2) (3,4) (3,5) (4,3) and, on larger panels, (5,4) and (9,11). (2,3) reproduced the retired two-lead generator exactly — identical validation block, areas and sliver count, only the report wording differs. That equivalence is the evidence the generalisation is faithful rather than plausible.

The limit is manufacturability, not topology, and it is worth being precise about what that means, because it is easy to mistake for a ceiling on leads. As leads rise, two things shrink: the lens regions between passes, and the rim gaps between anchors. Either falling under MIN_FEATURE gets it welded shut, and a welded region is one the count never sees.

At a fixed 30mm panel that looks like a hard stop. (5,2) reports 2 crossings against 8; (7,2) reports zero at default tuning; (5,4) caps at 17 regions against 21. But the shortfall is almost always exactly B — the rim gaps — and those scale with the panel:

So five leads and nine leads both work; they simply do not fit in a 30mm hole. What the tested envelope really describes is a relationship between lead count and panel size, not a maximum lead count.

Two things still need attention as leads rise:

Judge by the region and crossing counts, not by the sliver count — see the validation report for why.

Size is the other lever, and it works

(9,11) satisfies every invariant — but only on a large enough panel, and that is worth understanding because it is the clearest evidence for the claim above.

At the 30mm default it fails completely: 23 regions against 100, zero crossings found, 18.3% open. Nine passes of 4mm ribbon do not fit that annulus at all. A finer ribbon fixes the crossings — every working row below runs AMP/HW between 15 and 26, far above the L × HW figure — and from 30mm up the crossing count is a correct 88.

The region count is what stays wrong, and it stays wrong by exactly 11, which is B. The eleven rim gaps were forming below MIN_FEATURE and being welded shut. They scale with the panel, so at R_HOLE = 300 they clear the floor and survive:

R_HOLEAMPHWregions (want 100)crossings (want 88)open
307.52.023018.3%
3070.3788879.1%
100261.0898876.3%
300785.01008861.1%

So the ceiling on leads is not a property of the curve. It is the cutting floor meeting features that shrink as leads rise — and making the panel bigger moves those features back above it. Note also that the region count was the only line telling the truth at 100mm: crossings, engrave and anchors all read OK while eleven regions were quietly missing.

Output layers

Two groups, told apart by stroke colour as well as by id. Colour is the one that survives the trip: many SVG importers flatten groups, and most laser software assigns operations by colour rather than by group name.

GroupStrokePurpose
cutblack #000000every closed path is waste that drops out
engraveblue #0000ffover/under interlace hints (2*B*(L-1) lines) and rim continuations

Rim continuations

The engrave layer carries a second, purely decorative set of lines. Where the ribbon overruns the rim it fuses into the soundboard, and the cut layer stops dead at R_HOLE — beyond the rim there is nothing to cut. That leaves each anchor reading as a flat pad, and the rosette looking sliced off by a circle.

The ribbon's own edges do carry on out there, as far as R_HOLE + BITE. These lines put that outline back across the anchor, so the eye reads the ribbon as passing into the board rather than being truncated. On the trefoil they occupy radii 30.0 to 31.5mm — exactly the overrun band — while the interlace lines sit at 19.7 to 24.2mm.

They are reported separately, as rim continuations, and deliberately not folded into the 2*B*(L-1) count: that number is derived from crossings and is a topological check, and mixing a decorative addition into it would spoil a line that does real work. Their own count is not an invariant — roughly one or two per anchor, depending on where each run happens to break.

Adding them changed no cut geometry at all. Verified by regenerating and comparing: the cut group is byte-identical before and after. Raising BITE gives more overlap and a stronger effect, but BITE also sets how firmly the rosette is anchored, so it is not free.

The blue lines must not be cut. Give them a score or engrave operation, or delete the layer. They run straight across the ribbon at every crossing, so cutting them severs it and the rosette comes apart as it leaves the machine.

Give every colour you keep an explicit operation. A per-colour job silently skips any colour you leave unmapped: leave cut unmapped and you get an engraved picture of a sound hole and no hole.

Read the validation report

LineMust be
closed contours vs flood regionsequal, else loops were lost
unclosed chains0, else a contour leaked
crossings vs B*(L-1)equal — printed and checked on every run
engrave polylines vs 2*B*(L-1)equal — the second half of the crossing check, and the one that separates a pair which merely looks right from one that is. 9 × 10 and 9 × 11 pass it; 9 × 4, 9 × 7 and 9 × 8 get their regions and crossings right and fail here
weave alternatesOK, else the engrave layer is suppressed
slivers welded shutinformational only — not a pass/fail, and not normally 0
loose islands (CW)0 — else material falls out when cut
contour area vs flood areadelta ≤ ~0.1%
content vs canvasOK — every emitted point inside the viewBox, with the margin shown

slivers welded shut counts whole air regions narrower than MIN_FEATURE that were filled back to material. Its value is an artifact of where the sampling grid falls, not a property of the design: the trefoil reports 8 at NG=700, 6 at 1000, 4 at 1400 and 8 at 2000, while its contour count, open fraction and area hold steady. The region count is the number that means something — it is stable across resolutions, and when welding does eat a real lens it says so and keeps saying so at any grid.

content vs canvas is the newest line and the only one that looks at the document rather than the geometry. Every other check describes what the generator computed; none of them noticed when the rim continuations reached 1mm past a canvas sized for the cut layer alone, and files were shipped clipped while every invariant passed. It measures the emitted points against the viewBox and names the shortfall if there is one.

How the over/under is decided

The retired two-lead generator could use a closed form — "over if m is even" — because with two leads the single other pass sits exactly half the sample array away. With L leads a point has L-1 other passes, at offsets k*M/L, so that shortcut is gone.

Instead the crossing events are found numerically, ordered along the strand and labelled alternately, and then the pairing is verified: each crossing must receive one over and one under. It has held at every pair tested. If it ever fails, the engrave layer is dropped and the report says so, rather than drawing a weave that lies about itself.

Everything else — signed distance field, flood fill, sliver welding, marching squares, chaining, RDP, SVG emit — is carried over unchanged, including the spatial hash for distance.

Run SELFTEST if you touch the distance code

The expanding-ring stopping bound fails silently if wrong — it just returns distances that are too large, which reads as phantom extra "cut" area and quietly changes the shape without any validation line firing.

LEADS=3 BIGHTS=5 SELFTEST=1 node knot_soundhole.js

The code's pass gate is < 1e-9, but the error should be exactly 0: both paths minimise the same expression over the same sample set, so they return bit-identical floats. Any nonzero value at all means the ring search missed a sample — do not read a small error as rounding. It is 0 at (2,3), (3,5) and (4,3).

Traps already sprung here

Caveats

Passing the invariants is not the same as being cuttable. (4,7) satisfies both counts and still comes out finer than 4 × 3, which is flagged above as delicate. The region and crossing counts tell you the topology survived; they say nothing about whether a laser can make it. Read tightest region inradius from the report as well — double it for the narrowest cut, and compare that against your kerf before believing the OK lines.

Generated files

3 × 2 has only two rim anchors, the fewest any of these designs can have. It removes half the disc and holds the result on two 4mm tabs. Three anchors are described above as a kinematic minimum rather than a stiffness one; two is below that, so treat it as the most fragile thing here that still passes every invariant.

Two-lead knots also carry folk names, which you will meet in knot literature even though the generator does not use them: trefoil at 3 bights, cinquefoil at 5, septafoil at 7, nonafoil at 9. Everything here is named by leads and bights instead, in the report, in the SVG <title> and in the filename, so the three always agree.

One that is not a sound hole

This is a 600mm hole. Nothing has a sound hole that size — it is a decorative panel, a screen, a table inlay, and it is committed as a demonstration rather than as something to fit an instrument. Read the caveats before cutting it: