KerfScript language reference
Language version 0.1. Copyright (c) 2026 MyLaserTools. This document is licensed under CC BY-ND 4.0. "KerfScript" is a trademark of MyLaserTools, see TRADEMARKS.md.
KerfScript describes flat parts for laser cutting. A program is a list of statements that build 2D shapes, put them on layers (cut, score, engrave), group them into parts, and the renderer compensates for the kerf, packs parts onto sheets and writes SVG or DXF.
Units are millimetres and degrees. The y axis points up.
// A coaster with a hole, an engraved ring and a scored border.
size = 90; // [60:5:120]
difference() {
circle(d = size);
circle(d = 6);
}
engrave() difference() { circle(d = size - 16); circle(d = size - 22); }
score() circle(d = size - 6);Lexical structure
- Comments:
// lineand/* block */. - Numbers:
12,0.5,.5,1e3. - Strings:
"text"with\n,\t,\",\\and\uXXXXescapes. - Names: letters, digits and
_, not starting with a digit. Names starting with$are special variables (see Scope). - Keywords:
module function if else for let each use include true false undef.
Values
| Type | Examples |
|---|---|
| number | 3, -0.25, PI |
| string | "Oak" |
| boolean | true, false |
| undef | undef, a missing argument, an index past the end of a list |
| list | [1, 2], [[0, 0], [10, 0], [5, 8]] |
| range | [0:5] (0 to 5 step 1), [0:2:10] (0 to 10 step 2) |
| function | function(x) x * 2 |
Points are lists [x, y]. p.x and p.y read the first two items.
Expressions
From lowest to highest precedence:
| Operator | Meaning |
|---|---|
c ? a : b |
conditional |
|| |
or |
&& |
and |
== != |
equality, lists compare item by item |
< <= > >= |
compare numbers or strings |
+ - |
add or subtract numbers or equal length lists; + joins strings |
* / % |
multiply, divide, remainder; list times number scales; list times list is the dot product; matrix times vector |
-x !x |
negate, not |
a ^ b |
power, right associative and tighter than unary minus |
f(x) a[i] a.x |
call, index, member |
Mixing types an operator can't handle (a list plus a number, undef + 1)
stops the program with an error naming the line. Truthiness: false, 0,
"", [] and undef are false.
let (a = 1, b = a + 1) expr binds names for one expression.
List comprehensions
squares = [for (i = [1:5]) i * i];
odd = [for (i = [1:9]) if (i % 2) i];
grid = [for (x = [0:2], y = [0:2]) [x * 10, y * 10]];
flat = [each [1, 2], 3];Statements
name = expr; // assignment
function name(a, b = 2) = expr; // function
module name(a, b = 2) { ... } // module (makes shapes)
name(args) child; // module call with one child
name(args) { child; child; } // module call with a block of children
for (i = range_or_list, j = ...) ... // repeats, combined as a union
if (cond) ... else ...
let (a = 1) ...
use <path> // import modules and functions
include <path> // run another file in placeArguments can be positional or named: rect([20, 10], r = 2).
Scope
- Assignments run in order. A later assignment to the same name replaces the earlier one from that point on.
moduleandfunctiondefinitions are visible in their whole block, so you can call them before they are defined.- Blocks, loops and module bodies have their own scope.
- Special variables (
$name) are dynamically scoped: a value set by a caller is seen by everything it calls. Pass them as arguments too:circle(5, $fn = 6). - Top-level assignments are the program's parameters. Renderers override
them by name (
kerf render -D width=200).
Modifiers
Put before a module call: * disables it, # and % move it to the guide
layer so you can see it without cutting it.
children()
Inside a module, children() renders the statements the module was called
with, children(i) renders only the i-th one, and $children is their count.
module hang_hole(d = 5) {
difference() { children(); translate([0, -d * 1.5]) circle(d = d); }
}Special variables
| Name | Default | Meaning |
|---|---|---|
$thickness |
3 |
material thickness, used by joinery and stored on parts |
$kerf |
0 |
beam width; cut outlines are offset by half of it |
$material |
"material" |
material name; parts are packed per material and thickness |
$sheet |
[600, 400] |
sheet size for packing |
$gap |
3 |
space between packed parts and around the sheet edge |
$layout |
"pack" |
"pack" packs parts onto sheets, "none" keeps your placement |
$fn $fa $fs |
0 2 0.2 |
curve resolution: fixed segment count, or max angle and max segment length |
$font |
default font name for text() |
Layers
Every shape is on a layer. Shapes not wrapped in a layer module are on cut.
| Layer | Mode | Colour | What happens |
|---|---|---|---|
engrave |
fill | #000000 |
the area is engraved |
score |
line | #0000ff |
lines and shape outlines are marked, not cut through |
cut |
cut | #ff0000 |
outlines are cut through, with kerf compensation |
guide |
guide | #00a0ff |
shown only when guides are requested |
cut() ... engrave() ... score() ... guide() ...
layer("pocket", mode = "fill", color = "#00aa00") ...layer(name, mode, color) defines a layer. Modes are "cut", "line",
"fill" and "guide". Output lists layers as engrave, custom layers, score,
cut, guide, so machines engrave before cutting.
Rules applied when a part is finished:
- The part's material is the union of its cut layer shapes.
- Engrave, score and custom layers are clipped to the material, so nothing is marked inside holes or off the part. Score outlines become lines first.
- Cut lines (from
line(),living_hinge()and similar) are clipped to the material. - If the material breaks into more than one piece, the renderer warns.
- In the output, holes and inner cuts come before outer outlines.
Parts and sheets
material(thickness = 4, kerf = 0.12, name = "birch") {
part("side", qty = 2) finger_rect([100, 60], "out");
part("base") rect([100, 100]);
}part(name, qty = 1) { ... }makes a named part. It records$thickness,$kerfand$materialwhere it is called.- Shapes outside any part form one part named
part. material(thickness, kerf, name) { ... }sets those special variables for its children.- With
$layout = "pack"parts are grouped by material and thickness, copiedqtytimes, rotated 90° if that makes them fit, and shelf-packed onto sheets of$sheet. Parts larger than the sheet are reported.
Built-in modules
Shapes
| Module | Notes |
|---|---|
rect(size, center = false, r = 0) |
size is a number or [w, h]; r rounds the corners. square is an alias |
circle(r) or circle(d = ...) |
|
ellipse([rx, ry]) |
|
regular_polygon(n, r) |
corner radius r |
star(n, r, inner) |
|
polygon(points, paths) |
even-odd fill, paths lists point indexes per outline |
path(d, fill = "evenodd") |
SVG path data; closed subpaths become shapes, open ones lines |
text(text, size = 10, font, halign, valign, spacing = 1) |
halign: left, center, right; valign: baseline, top, center, bottom |
line(from, to) |
a line, not an area |
polyline(points, closed = false) |
|
arc(r, start = 0, end = 90) |
arc line, angles in degrees |
Transforms
translate([x, y]), rotate(a, center = [x, y]), scale(s) or
scale([sx, sy]), mirror([nx, ny]) (mirror across the line through the
origin with that normal), multmatrix(m) with a 2x3 matrix.
Combining
| Module | Notes |
|---|---|
union() / group() |
joins children |
difference() |
removes every later child's area from every layer of the first child |
intersection() |
keeps only the area common to all children, layers of the first child kept |
hull() |
convex hull |
offset(r) / offset(delta, chamfer = false) |
grow (positive) or shrink areas; r rounds corners |
stroke(width) |
turns lines into areas of that width, for cutting single line art |
outline() |
turns areas into their outline lines |
Joinery
| Module | Notes |
|---|---|
finger_rect(size, edges, finger) |
rectangle with finger joints. edges is one value or [bottom, right, top, left], each "out", "in" or "flat". An "out" edge keeps its corners, an "in" edge notches them; join an "out" edge to an "in" edge of the same length. Notch depth is $thickness; finger defaults to max(3 * $thickness, 6) and the count is always odd |
finger_holes(length, edge = "out", finger) |
row of slots along x, centred on y = 0, that an edge finger edge of the same length plugs into (for shelves and dividers) |
tslot(d = 3, length = 12, nut = [w, h], nut_at) |
T-slot for a screw and nut, opening at y = 0 and running up; subtract it from a panel edge |
living_hinge(size, spacing = 1.5, bridge = 3, cut) |
straight-line flex pattern as cut lines filling [w, h] |
Other
echo(...) prints values, assert(cond, message) stops with the message when
cond is false, children(i).
Built-in functions
Math (degrees): abs sign floor ceil round sqrt pow exp ln log sin cos tan asin acos atan atan2 min max clamp lerp norm cross.
Lists and strings: len concat reverse sum slice str chr ord upper lower split join search contains lookup.
Types: is_num is_string is_bool is_list is_undef is_function type_of.
Random: rands(min, max, count, seed) is repeatable for the same seed.
Text: text_width(text, size, font) returns the advance width;
text_metrics(text, size, font) returns [advance, [[x0, y0], [x1, y1]], ascent, descent].
Joinery: finger_count(length, finger) returns the finger count
finger_rect uses for that edge.
Customizer comments
Top-level assignments with a plain value (number, string, boolean or list of
numbers) are listed as parameters by kerf params and by editors:
/* [Size] */ // group for the following parameters
// Outer width <- caption: the comment line above
width = 120; // [40:5:400] min:step:max
depth = 80; // [200] max
shape = "round"; // [round:Round, square:Square] choices with labels
count = 3; // [2, 3, 4] choices
/* [Hidden] */ // not listedThe format matches OpenSCAD customizer comments, so existing editors can show KerfScript parameters.
Standard library
use <kerf/box>:
box([w, d, h], lid = false, finger)makes the six (or five) panels of a finger-jointed box as partsbottom,lid,front,back,left,right. Sizes are outer sizes.divider([w, h], finger)makes a divider with finger tabs on both sides.
Output
- SVG: millimetre units, one Inkscape layer group per KerfScript layer,
colours as above, 0.1 mm hairline strokes,
data-parton every path. - DXF: R12 (AC1009) polylines on layers named after KerfScript layers, with matching AutoCAD colours.
Limits
Renderers stop programs that recurse deeper than about a thousand calls, loop over more than a million items, or run more than five million steps, with an error instead of hanging.